Compare commits

...

36 Commits

Author SHA1 Message Date
snyk-bot
57cf57ac11 fix: upgrade @smithy/fetch-http-handler from 3.2.4 to 3.2.9
Snyk has created this PR to upgrade @smithy/fetch-http-handler from 3.2.4 to 3.2.9.

See this package in npm:
@smithy/fetch-http-handler

See this project in Snyk:
https://app.snyk.io/org/vrtmrz/project/d2c9b72d-6e38-433f-bbad-725719c0fa4d?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-11-20 12:55:38 +00:00
vorotamoroz
ed5cb3e043 Format utility 2024-11-12 01:09:07 +00:00
vorotamoroz
574fdf9202 Format submodule 2024-11-12 01:08:35 +00:00
vorotamoroz
9ec7b809a9 I have adapted the new-line-char to the codebase. But I am not sure if this is the right thing to do. 2024-11-12 01:04:46 +00:00
vorotamoroz
4d302aff9d Merge pull request #533 from doublethefish/chore/formatting
chore(formatting): adds prettier for consistent coding style
2024-11-12 09:53:50 +09:00
vorotamoroz
b70009f4a9 Merge branch 'main' into chore/formatting 2024-11-12 09:37:52 +09:00
Frank Harrison
c24ee32f37 chore(formatting): ignores generated code in pretty 2024-11-11 09:45:53 +00:00
Frank Harrison
012d0aa4df chore(format): also format json (no effect)
Adds json-file formating to the prettier command. Currently this has no
effect, but if the `tabWidth` is changed to 2 and the `printWidth`
reduced to 88, then it will also impact the json files and keep the
formatting consistent across code and data.
2024-11-11 09:41:56 +00:00
Frank Harrison
5c97e5b672 chore(format): no intentional behaviour change - runs pretty 2024-11-11 09:41:52 +00:00
Frank Harrison
6e1eb36f3b chore(format): adds prettier and commands to run it
... including ci/cd check-only commands.

The code is quite complex and missing a lot of dev-ops types checks and
standards. One of the key problems with codebases like this is
on-boarding new developers to the codebase (like myself). When there is
a consistent and enforced coding-style (irrespective of what the coding
style is) it makes it _significantly_ easier for collaborators and
maintainers to get on with the job in hand. It also, from a day-2-day
developer perspective, significantly reduces cognitive overhead re
reading code.
Finally this is a "trial balloon" PR, if this patch is accepted I will
likely do more work on testing and docs for the project.

- The new prettier config is a non-standard setup, but a close-match to
  how the code _currently_ looks.
- 120 col-width print width (instead of the better and more
  information-dense 88), this is so the diff after applying prettier to
  the code is less disruptive, whilst still showing the benefits of using
  a prettier.
- We use `tabWidth` setting of 4 as the code uses that more common
  setting instead of the more compact 2 spaces - note that 2 often leads
  to more readable and compact code.
- We enforce trailing commas, as that seems to be the norm in this
  code-base. We choose the `es5` standard here.
- We enforce tailing semi-colons (`semi`) as the majority of code used
  that flavour of `js`/`ts`.
- For now we only run on code and not json files.

This is designed such that `npm run pretty` re-formats the code for
development, and when integrated with ci/cd, `prettyCheck` will return
non-zero exit codes when formatting doesn't match the coding standards.
2024-11-11 09:41:35 +00:00
vorotamoroz
8809aee327 Add default values to a new SetupURI. 2024-11-11 08:05:10 +00:00
vorotamoroz
fc04c557fc Merge branch 'main' of https://github.com/vrtmrz/obsidian-livesync 2024-11-11 01:23:07 +00:00
vorotamoroz
115a0d2d8a bump 2024-11-11 01:22:40 +00:00
vorotamoroz
2c97289ec8 Fixed
-   Vault History can show the correct information of match-or-not for each file and database even if it is a binary file.
-   `Sync settings via markdown` is now hidden during the setup wizard.
-   Verify and Fix will ignore the hidden files if the hidden file sync is disabled.

New feature
-   Now we can fetch the tweaks from the remote database while the setting dialogue and wizard are processing.

Improved
-   More things are moved to the modules.
    -   Includes the Main codebase. Now `main.ts` is almost stub.
-   EventHub is now more robust and typesafe.
2024-11-11 00:58:31 +00:00
vorotamoroz
6d472d17fd Fix output order 2 2024-11-09 10:31:34 +09:00
vorotamoroz
3a3aabfd11 Fix scripting 2024-11-09 10:21:05 +09:00
vorotamoroz
8b45dd1d24 .. 2024-11-08 11:12:35 +00:00
vorotamoroz
a2b36ccf31 v0.24.0! 2024-10-28 11:18:29 +09:00
vorotamoroz
25e30fa09d Merge tag '0.24.0.dev-rc8' 2024-10-28 10:24:38 +09:00
vorotamoroz
8f5bc387b4 Update manifest-beta.json 2024-10-25 20:44:32 +09:00
vorotamoroz
5afe24c460 0.24.0.dev-rc8 2024-10-25 12:39:32 +01:00
vorotamoroz
658a09f1cc Update manifest-beta.json 2024-10-24 20:46:08 +09:00
vorotamoroz
a9020a3aea 0.24.0.dev-rc7 2024-10-24 12:41:50 +01:00
vorotamoroz
293c731437 Update manifest-beta.json 2024-10-22 18:11:18 +09:00
vorotamoroz
5b4ae37030 0.24.0.dev-rc6 2024-10-22 10:08:20 +01:00
vorotamoroz
9e8d126259 Update manifest-beta.json 2024-10-21 17:52:28 +09:00
vorotamoroz
6d244a6e34 0.24.0.dev-rc5 2024-10-21 09:47:09 +01:00
vorotamoroz
1f0ad4eb1e Update manifest-beta.json 2024-10-18 19:23:48 +09:00
vorotamoroz
e0e0ab0426 0.24.0.dev-rc4 2024-10-18 11:14:58 +01:00
vorotamoroz
5023d6da0b Merge pull request #474 from nyawox/per-file-sync-grammar
fix: per-file customization sync description grammar
2024-10-18 11:43:03 +09:00
vorotamoroz
49160c7d57 Merge branch 'main' into per-file-sync-grammar 2024-10-18 11:42:49 +09:00
vorotamoroz
4434224c29 Merge pull request #479 from fuhrysteve/patch-1
add username and password to setup URI instructions
2024-10-18 11:40:23 +09:00
vorotamoroz
cf3b9e5522 Add manifest beta 2024-10-17 10:26:19 +01:00
vorotamoroz
7ca5ac5ac7 0.24.0.dev-rc3 2024-10-17 10:19:08 +01:00
Stephen J. Fuhry
f778107727 add username and password to setup URI instructions 2024-08-07 14:43:38 -04:00
nyawox
12d825ea49 fix: per-file customization sync description grammar 2024-07-26 23:38:01 +09:00
87 changed files with 7385 additions and 4475 deletions

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
pouchdb-browser.js
main_org.js

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"printWidth": 120,
"semi": true,
"endOfLine": "crlf"
}

View File

@@ -106,6 +106,8 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv
$ export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com #Point to your vault
$ export database=obsidiannotes #Please change as you like
$ export passphrase=dfsapkdjaskdjasdas #Please change as you like
$ export username=johndoe
$ export password=abc123
$ deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts
obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D
@@ -206,4 +208,4 @@ entryPoints:
address: ":443"
...
```
```

View File

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

View File

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

99
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "obsidian-livesync",
"version": "0.24.0.dev-rc2",
"version": "0.24.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.24.0.dev-rc2",
"version": "0.24.1",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.645.0",
"@smithy/fetch-http-handler": "^3.2.4",
"@smithy/fetch-http-handler": "^3.2.9",
"@smithy/protocol-http": "^4.1.0",
"@smithy/querystring-builder": "^3.0.3",
"diff-match-patch": "^1.0.5",
@@ -58,6 +58,7 @@
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "^3.3.3",
"svelte": "^4.2.19",
"svelte-preprocess": "^6.0.2",
"terser": "^5.31.6",
@@ -1831,14 +1832,14 @@
}
},
"node_modules/@smithy/fetch-http-handler": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.4.tgz",
"integrity": "sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==",
"version": "3.2.9",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz",
"integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/protocol-http": "^4.1.0",
"@smithy/querystring-builder": "^3.0.3",
"@smithy/types": "^3.3.0",
"@smithy/protocol-http": "^4.1.4",
"@smithy/querystring-builder": "^3.0.7",
"@smithy/types": "^3.5.0",
"@smithy/util-base64": "^3.0.0",
"tslib": "^2.6.2"
}
@@ -2049,12 +2050,12 @@
}
},
"node_modules/@smithy/protocol-http": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.0.tgz",
"integrity": "sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.7.tgz",
"integrity": "sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^3.3.0",
"@smithy/types": "^3.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -2062,11 +2063,12 @@
}
},
"node_modules/@smithy/querystring-builder": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz",
"integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==",
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.10.tgz",
"integrity": "sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^3.3.0",
"@smithy/types": "^3.7.1",
"@smithy/util-uri-escape": "^3.0.0",
"tslib": "^2.6.2"
},
@@ -2149,9 +2151,10 @@
}
},
"node_modules/@smithy/types": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz",
"integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.1.tgz",
"integrity": "sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
},
@@ -5690,6 +5693,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -7893,13 +7912,13 @@
}
},
"@smithy/fetch-http-handler": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.4.tgz",
"integrity": "sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==",
"version": "3.2.9",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz",
"integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==",
"requires": {
"@smithy/protocol-http": "^4.1.0",
"@smithy/querystring-builder": "^3.0.3",
"@smithy/types": "^3.3.0",
"@smithy/protocol-http": "^4.1.4",
"@smithy/querystring-builder": "^3.0.7",
"@smithy/types": "^3.5.0",
"@smithy/util-base64": "^3.0.0",
"tslib": "^2.6.2"
}
@@ -8061,20 +8080,20 @@
}
},
"@smithy/protocol-http": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.0.tgz",
"integrity": "sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.7.tgz",
"integrity": "sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg==",
"requires": {
"@smithy/types": "^3.3.0",
"@smithy/types": "^3.7.1",
"tslib": "^2.6.2"
}
},
"@smithy/querystring-builder": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz",
"integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==",
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.10.tgz",
"integrity": "sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ==",
"requires": {
"@smithy/types": "^3.3.0",
"@smithy/types": "^3.7.1",
"@smithy/util-uri-escape": "^3.0.0",
"tslib": "^2.6.2"
}
@@ -8134,9 +8153,9 @@
}
},
"@smithy/types": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz",
"integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.1.tgz",
"integrity": "sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==",
"requires": {
"tslib": "^2.6.2"
}
@@ -10787,6 +10806,12 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true
},
"psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.24.0.dev-rc2",
"version": "0.24.1",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",
@@ -11,6 +11,9 @@
"lint": "eslint src",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
"tsc-check": "tsc --noEmit",
"pretty": "npm run prettyNoWrite -- --write --log-level error",
"prettyCheck": "npm run prettyNoWrite -- --check",
"prettyNoWrite": "prettier --config ./.prettierrc \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
"check": "npm run lint && npm run svelte-check && npm run tsc-check"
},
"keywords": [],
@@ -51,6 +54,7 @@
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "^3.3.3",
"svelte": "^4.2.19",
"svelte-preprocess": "^6.0.2",
"terser": "^5.31.6",
@@ -60,7 +64,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.645.0",
"@smithy/fetch-http-handler": "^3.2.4",
"@smithy/fetch-http-handler": "^3.2.9",
"@smithy/protocol-http": "^4.1.0",
"@smithy/querystring-builder": "^3.0.3",
"diff-match-patch": "^1.0.5",

View File

@@ -1,3 +1,7 @@
import type { FilePathWithPrefix, ObsidianLiveSyncSettings } from "../lib/src/common/types";
import { eventHub } from "../lib/src/hub/hub";
import type ObsidianLiveSyncPlugin from "../main";
export const EVENT_LAYOUT_READY = "layout-ready";
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
export const EVENT_PLUGIN_UNLOADED = "plugin-unloaded";
@@ -9,20 +13,37 @@ export const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed";
export const EVENT_LOG_ADDED = "log-added";
export const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings";
export const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard";
export const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri";
export const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri";
export const EVENT_REQUEST_SHOW_HISTORY = "show-history";
export const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab";
export const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog";
// export const EVENT_FILE_CHANGED = "file-changed";
import { eventHub } from "../lib/src/hub/hub";
// TODO: Add overloads for the emit method to allow for type checking
declare global {
interface LSEvents {
[EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG]: undefined;
[EVENT_FILE_SAVED]: undefined;
[EVENT_REQUEST_OPEN_SETUP_URI]: undefined;
[EVENT_REQUEST_COPY_SETUP_URI]: undefined;
[EVENT_REQUEST_RELOAD_SETTING_TAB]: undefined;
[EVENT_PLUGIN_UNLOADED]: undefined;
[EVENT_SETTING_SAVED]: ObsidianLiveSyncSettings;
[EVENT_PLUGIN_LOADED]: ObsidianLiveSyncPlugin;
[EVENT_LAYOUT_READY]: undefined;
"event-file-changed": { file: FilePathWithPrefix; automated: boolean };
"document-stub-created": {
toc: Set<string>;
stub: { [key: string]: { [key: string]: Map<string, Record<string, string>> } };
};
[EVENT_REQUEST_OPEN_SETTINGS]: undefined;
[EVENT_REQUEST_OPEN_SETTING_WIZARD]: undefined;
[EVENT_FILE_RENAMED]: { newPath: FilePathWithPrefix; old: FilePathWithPrefix };
[EVENT_LEAF_ACTIVE_CHANGED]: undefined;
}
}
export { eventHub };

View File

@@ -0,0 +1,12 @@
import type { TFile } from "../deps";
import type { FilePathWithPrefix, LoadedEntry } from "../lib/src/common/types";
export const EVENT_REQUEST_SHOW_HISTORY = "show-history";
declare global {
interface LSEvents {
[EVENT_REQUEST_SHOW_HISTORY]:
| { file: TFile; fileOnDB: LoadedEntry }
| { file: FilePathWithPrefix; fileOnDB: LoadedEntry };
}
}

View File

@@ -1,5 +1,11 @@
import { type PluginManifest, TFile } from "../deps.ts";
import { type DatabaseEntry, type EntryBody, type FilePath, type UXFileInfoStub, type UXInternalFileInfoStub } from "../lib/src/common/types.ts";
import {
type DatabaseEntry,
type EntryBody,
type FilePath,
type UXFileInfoStub,
type UXInternalFileInfoStub,
} from "../lib/src/common/types.ts";
export interface PluginDataEntry extends DatabaseEntry {
deviceVaultName: string;
@@ -55,15 +61,15 @@ export type FileEventArgs = {
cache?: CacheData;
oldPath?: string;
ctx?: any;
}
};
export type FileEventItem = {
type: FileEventType,
args: FileEventArgs,
key: string,
skipBatchWait?: boolean,
cancelled?: boolean,
batched?: boolean
}
type: FileEventType;
args: FileEventArgs;
key: string;
skipBatchWait?: boolean;
cancelled?: boolean;
batched?: boolean;
};
// Hidden items (Now means `chunk`)
export const CHeader = "h:";
@@ -82,4 +88,3 @@ export const ICXHeader = "ix:";
export const FileWatchEventQueueMax = 10;
export const configURIBase = "obsidian://setuplivesync?settings=";

View File

@@ -1,20 +1,48 @@
import { normalizePath, Platform, TAbstractFile, type RequestUrlParam, requestUrl } from "../deps.ts";
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "../lib/src/string_and_binary/path.ts";
import {
path2id_base,
id2path_base,
isValidFilenameInLinux,
isValidFilenameInDarwin,
isValidFilenameInWidows,
isValidFilenameInAndroid,
stripAllPrefixes,
} from "../lib/src/string_and_binary/path.ts";
import { Logger } from "../lib/src/common/logger.ts";
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix, type UXFileInfo, type UXFileInfoStub } from "../lib/src/common/types.ts";
import {
LOG_LEVEL_VERBOSE,
type AnyEntry,
type DocumentID,
type EntryHasPath,
type FilePath,
type FilePathWithPrefix,
type UXFileInfo,
type UXFileInfoStub,
} from "../lib/src/common/types.ts";
import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types.ts";
import type ObsidianLiveSyncPlugin from "../main.ts";
import { writeString } from "../lib/src/string_and_binary/convert.ts";
import { fireAndForget } from "../lib/src/common/utils.ts";
import { sameChangePairs } from "./stores.ts";
export { scheduleTask, setPeriodicTask, cancelTask, cancelAllTasks, cancelPeriodicTask, cancelAllPeriodicTask, } from "../lib/src/concurrency/task.ts";
export {
scheduleTask,
setPeriodicTask,
cancelTask,
cancelAllTasks,
cancelPeriodicTask,
cancelAllPeriodicTask,
} from "../lib/src/concurrency/task.ts";
// For backward compatibility, using the path for determining id.
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
// The first slash will be deleted when the path is normalized.
export async function path2id(filename: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false, caseInsensitive: boolean): Promise<DocumentID> {
export async function path2id(
filename: FilePathWithPrefix | FilePath,
obfuscatePassphrase: string | false,
caseInsensitive: boolean
): Promise<DocumentID> {
const temp = filename.split(":");
const path = temp.pop();
const normalizedPath = normalizePath(path as FilePath);
@@ -35,7 +63,6 @@ export function id2path(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefi
}
export function getPath(entry: AnyEntry) {
return id2path(entry._id, entry);
}
export function getPathWithoutPrefix(entry: AnyEntry) {
const f = getPath(entry);
@@ -50,7 +77,6 @@ export function getPathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWi
return file.path;
}
const memos: { [key: string]: any } = {};
export function memoObject<T>(key: string, obj: T): T {
memos[key] = obj;
@@ -59,7 +85,7 @@ export function memoObject<T>(key: string, obj: T): T {
export async function memoIfNotExist<T>(key: string, func: () => T | Promise<T>): Promise<T> {
if (!(key in memos)) {
const w = func();
const v = w instanceof Promise ? (await w) : w;
const v = w instanceof Promise ? await w : w;
memos[key] = v;
}
return memos[key] as T;
@@ -75,7 +101,6 @@ export function disposeMemoObject(key: string) {
delete memos[key];
}
export function isValidPath(filename: string) {
if (Platform.isDesktop) {
// if(Platform.isMacOS) return isValidFilenameInDarwin(filename);
@@ -94,11 +119,10 @@ export function trimPrefix(target: string, prefix: string) {
return target.startsWith(prefix) ? target.substring(prefix.length) : target;
}
/**
* returns is internal chunk of file
* @param id ID
* @returns
* @returns
*/
export function isInternalMetadata(id: FilePath | FilePathWithPrefix | DocumentID): boolean {
return id.startsWith(ICHeader);
@@ -107,7 +131,7 @@ export function stripInternalMetadataPrefix<T extends FilePath | FilePathWithPre
return id.substring(ICHeaderLength) as T;
}
export function id2InternalMetadataId(id: DocumentID): DocumentID {
return ICHeader + id as DocumentID;
return (ICHeader + id) as DocumentID;
}
// const CHeaderLength = CHeader.length;
@@ -122,7 +146,6 @@ export function isCustomisationSyncMetadata(str: string): boolean {
return str.startsWith(ICXHeader);
}
export class PeriodicProcessor {
_process: () => Promise<any>;
_timer?: number;
@@ -141,12 +164,16 @@ export class PeriodicProcessor {
enable(interval: number) {
this.disable();
if (interval == 0) return;
this._timer = window.setInterval(() => fireAndForget(async () => {
await this.process();
if (this._plugin._unloaded) {
this.disable();
}
}), interval);
this._timer = window.setInterval(
() =>
fireAndForget(async () => {
await this.process();
if (this._plugin.$$isUnloaded()) {
this.disable();
}
}),
interval
);
this._plugin.registerInterval(this._timer);
}
disable() {
@@ -157,11 +184,21 @@ export class PeriodicProcessor {
}
}
export const _requestToCouchDBFetch = async (baseUri: string, username: string, password: string, path?: string, body?: string | any, method?: string) => {
export const _requestToCouchDBFetch = async (
baseUri: string,
username: string,
password: string,
path?: string,
body?: string | any,
method?: string
) => {
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
const encoded = window.btoa(utf8str);
const authHeader = "Basic " + encoded;
const transformedHeaders: Record<string, string> = { authorization: authHeader, "content-type": "application/json" };
const transformedHeaders: Record<string, string> = {
authorization: authHeader,
"content-type": "application/json",
};
const uri = `${baseUri}/${path}`;
const requestParam = {
url: uri,
@@ -171,9 +208,17 @@ export const _requestToCouchDBFetch = async (baseUri: string, username: string,
body: JSON.stringify(body),
};
return await fetch(uri, requestParam);
}
};
export const _requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, path?: string, body?: any, method?: string) => {
export const _requestToCouchDB = async (
baseUri: string,
username: string,
password: string,
origin: string,
path?: string,
body?: any,
method?: string
) => {
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
const encoded = window.btoa(utf8str);
const authHeader = "Basic " + encoded;
@@ -187,23 +232,32 @@ export const _requestToCouchDB = async (baseUri: string, username: string, passw
body: body ? JSON.stringify(body) : undefined,
};
return await requestUrl(requestParam);
}
export const requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string = "", key?: string, body?: string, method?: string) => {
};
export const requestToCouchDB = async (
baseUri: string,
username: string,
password: string,
origin: string = "",
key?: string,
body?: string,
method?: string
) => {
const uri = `_node/_local/_config${key ? "/" + key : ""}`;
return await _requestToCouchDB(baseUri, username, password, origin, uri, body, method);
};
export const BASE_IS_NEW = Symbol("base");
export const TARGET_IS_NEW = Symbol("target");
export const EVEN = Symbol("even");
// Why 2000? : ZIP FILE Does not have enough resolution.
const resolution = 2000;
export function compareMTime(baseMTime: number, targetMTime: number): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
const truncatedBaseMTime = (~~(baseMTime / resolution)) * resolution;
const truncatedTargetMTime = (~~(targetMTime / resolution)) * resolution;
export function compareMTime(
baseMTime: number,
targetMTime: number
): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
const truncatedBaseMTime = ~~(baseMTime / resolution) * resolution;
const truncatedTargetMTime = ~~(targetMTime / resolution) * resolution;
// Logger(`Resolution MTime ${truncatedBaseMTime} and ${truncatedTargetMTime} `, LOG_LEVEL_VERBOSE);
if (truncatedBaseMTime == truncatedTargetMTime) return EVEN;
if (truncatedBaseMTime > truncatedTargetMTime) return BASE_IS_NEW;
@@ -215,7 +269,7 @@ export function markChangesAreSame(file: AnyEntry | string | UXFileInfoStub, mti
if (mtime1 === mtime2) return true;
const key = typeof file == "string" ? file : "_id" in file ? file._id : file.path;
const pairs = sameChangePairs.get(key, []) || [];
if (pairs.some(e => e == mtime1 || e == mtime2)) {
if (pairs.some((e) => e == mtime1 || e == mtime2)) {
sameChangePairs.set(key, [...new Set([...pairs, mtime1, mtime2])]);
} else {
sameChangePairs.set(key, [mtime1, mtime2]);
@@ -224,17 +278,20 @@ export function markChangesAreSame(file: AnyEntry | string | UXFileInfoStub, mti
export function isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | string, mtimes: number[]) {
const key = typeof file == "string" ? file : "_id" in file ? file._id : file.path;
const pairs = sameChangePairs.get(key, []) || [];
if (mtimes.every(e => pairs.indexOf(e) !== -1)) {
if (mtimes.every((e) => pairs.indexOf(e) !== -1)) {
return EVEN;
}
}
export function compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
export function compareFileFreshness(
baseFile: UXFileInfoStub | AnyEntry | undefined,
checkTarget: UXFileInfo | AnyEntry | undefined
): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
if (baseFile === undefined && checkTarget == undefined) return EVEN;
if (baseFile == undefined) return TARGET_IS_NEW;
if (checkTarget == undefined) return BASE_IS_NEW;
const modifiedBase = "stat" in baseFile ? baseFile?.stat?.mtime ?? 0 : baseFile?.mtime ?? 0;
const modifiedTarget = "stat" in checkTarget ? checkTarget?.stat?.mtime ?? 0 : checkTarget?.mtime ?? 0;
const modifiedBase = "stat" in baseFile ? (baseFile?.stat?.mtime ?? 0) : (baseFile?.mtime ?? 0);
const modifiedTarget = "stat" in checkTarget ? (checkTarget?.stat?.mtime ?? 0) : (checkTarget?.mtime ?? 0);
if (modifiedBase && modifiedTarget && isMarkedAsSameChanges(baseFile, [modifiedBase, modifiedTarget])) {
return EVEN;
@@ -242,21 +299,27 @@ export function compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undef
return compareMTime(modifiedBase, modifiedTarget);
}
const _cached = new Map<string, {
value: any;
context: Map<string, any>;
}>();
const _cached = new Map<
string,
{
value: any;
context: Map<string, any>;
}
>();
export type MemoOption = {
key: string;
forceUpdate?: boolean;
validator?: (context: Map<string, any>) => boolean;
}
};
export function useMemo<T>({ key, forceUpdate, validator }: MemoOption, updateFunc: (context: Map<string, any>, prev: T) => T): T {
export function useMemo<T>(
{ key, forceUpdate, validator }: MemoOption,
updateFunc: (context: Map<string, any>, prev: T) => T
): T {
const cached = _cached.get(key);
const context = cached?.context || new Map<string, any>();
if (cached && !forceUpdate && (!validator || validator && !validator(context))) {
if (cached && !forceUpdate && (!validator || (validator && !validator(context)))) {
return cached.value;
}
const value = updateFunc(context, cached?.value);
@@ -267,11 +330,14 @@ export function useMemo<T>({ key, forceUpdate, validator }: MemoOption, updateFu
}
// const _static = new Map<string, any>();
const _staticObj = new Map<string, {
value: any
}>();
const _staticObj = new Map<
string,
{
value: any;
}
>();
export function useStatic<T>(key: string): { value: (T | undefined) };
export function useStatic<T>(key: string): { value: T | undefined };
export function useStatic<T>(key: string, initial: T): { value: T };
export function useStatic<T>(key: string, initial?: T) {
// if (!_static.has(key) && initial) {
@@ -288,9 +354,9 @@ export function useStatic<T>(key: string, initial?: T) {
return this._buf as T;
},
set value(value: T) {
this._buf = value
}
}
this._buf = value;
},
};
_staticObj.set(key, obj);
return obj;
}
@@ -310,4 +376,4 @@ export function displayRev(rev: string) {
// export function getPathFromUXFileInfo(file: UXFileInfoStub | UXFileInfo | string) {
// return (typeof file == "string" ? file : file.path) as FilePathWithPrefix;
// }
// }

View File

@@ -1,13 +1,39 @@
import { type FilePath } from "./lib/src/common/types.ts";
export {
addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
parseYaml, ItemView, WorkspaceLeaf
addIcon,
App,
debounce,
Editor,
FuzzySuggestModal,
MarkdownRenderer,
MarkdownView,
Modal,
Notice,
Platform,
Plugin,
PluginSettingTab,
requestUrl,
sanitizeHTMLToDom,
Setting,
stringifyYaml,
TAbstractFile,
TextAreaComponent,
TFile,
TFolder,
parseYaml,
ItemView,
WorkspaceLeaf,
} from "obsidian";
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse, MarkdownFileInfo, ListedFiles } from "obsidian";
import {
normalizePath as normalizePath_
export type {
DataWriteOptions,
PluginManifest,
RequestUrlParam,
RequestUrlResponse,
MarkdownFileInfo,
ListedFiles,
} from "obsidian";
import { normalizePath as normalizePath_ } from "obsidian";
const normalizePath = normalizePath_ as <T extends string | FilePath>(from: T) => T;
export { normalizePath }
export { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
export { normalizePath };
export { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { ConfigSync, PluginDataExDisplayV2, type IPluginDataExDisplay } from "./CmdConfigSync.ts";
import { ConfigSync, PluginDataExDisplayV2, type IPluginDataExDisplay, type PluginDataExFile } from "./CmdConfigSync.ts";
import { Logger } from "../../lib/src/common/logger";
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types";
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
@@ -287,9 +287,17 @@
menu.addItem((item) => item.setTitle("Compare file").setIsLabel(true));
menu.addSeparator();
const files = unique(local.files.map((e) => e.filename).concat(selectedItem.files.map((e) => e.filename)));
const convDate = (dt: PluginDataExFile | undefined) => {
if (!dt) return "(Missing)";
const d = new Date(dt.mtime);
return d.toLocaleString();
};
for (const filename of files) {
menu.addItem((item) => {
item.setTitle(filename).onClick((e) => compareItems(local, selectedItem, filename));
const localFile = local.files.find((e) => e.filename == filename);
const remoteFile = selectedItem.files.find((e) => e.filename == filename);
const title = `${filename} (${convDate(localFile)} <--> ${convDate(remoteFile)})`;
item.setTitle(title).onClick((e) => compareItems(local, selectedItem, filename));
});
}
menu.showAtMouseEvent(evt);

View File

@@ -18,10 +18,11 @@ export class PluginDialogModal extends Modal {
this.contentEl.style.overflow = "auto";
this.contentEl.style.display = "flex";
this.contentEl.style.flexDirection = "column";
this.titleEl.setText("Customization Sync (Beta3)")
this.titleEl.setText("Customization Sync (Beta3)");
if (!this.component) {
this.component = new PluginPane({
target: contentEl, props: { plugin: this.plugin },
target: contentEl,
props: { plugin: this.plugin },
});
}
}
@@ -32,4 +33,4 @@ export class PluginDialogModal extends Modal {
this.component = undefined;
}
}
}
}

View File

@@ -12,7 +12,7 @@
export let plugin: ObsidianLiveSyncPlugin;
$: hideNotApplicable = false;
$: thisTerm = plugin.deviceAndVaultName;
$: thisTerm = plugin.$$getDeviceAndVaultName();
const addOn = plugin.getAddOn(ConfigSync.name) as ConfigSync;
if (!addOn) {

View File

@@ -16,10 +16,18 @@ export class JsonResolveModal extends Modal {
hideLocal: boolean;
title: string = "Conflicted Setting";
constructor(app: App, filename: FilePath,
docs: LoadedEntry[], callback: (keepRev?: string, mergedStr?: string) => Promise<void>,
nameA?: string, nameB?: string, defaultSelect?: string,
keepOrder?: boolean, hideLocal?: boolean, title: string = "Conflicted Setting") {
constructor(
app: App,
filename: FilePath,
docs: LoadedEntry[],
callback: (keepRev?: string, mergedStr?: string) => Promise<void>,
nameA?: string,
nameB?: string,
defaultSelect?: string,
keepOrder?: boolean,
hideLocal?: boolean,
title: string = "Conflicted Setting"
) {
super(app);
this.callback = callback;
this.filename = filename;
@@ -32,9 +40,12 @@ export class JsonResolveModal extends Modal {
this.hideLocal = hideLocal ?? false;
void waitForSignal(`cancel-internal-conflict:${filename}`).then(() => this.close());
}
async UICallback(keepRev?: string, mergedStr?: string) {
if (this.callback) {
await this.callback(keepRev, mergedStr);
}
this.close();
await this.callback?.(keepRev, mergedStr);
this.callback = undefined;
}
@@ -54,14 +65,14 @@ export class JsonResolveModal extends Modal {
defaultSelect: this.defaultSelect,
keepOrder: this.keepOrder,
hideLocal: this.hideLocal,
callback: (keepRev: string | undefined, mergedStr: string | undefined) => this.UICallback(keepRev, mergedStr),
callback: (keepRev: string | undefined, mergedStr: string | undefined) =>
this.UICallback(keepRev, mergedStr),
},
});
}
return;
}
onClose() {
const { contentEl } = this;
contentEl.empty();

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,17 @@
import { Logger } from "octagonal-wheels/common/logger";
import { getPath } from "../common/utils.ts";
import { type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "../lib/src/common/types.ts";
import {
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
type AnyEntry,
type DocumentID,
type EntryHasPath,
type FilePath,
type FilePathWithPrefix,
type LOG_LEVEL,
} from "../lib/src/common/types.ts";
import type ObsidianLiveSyncPlugin from "../main.ts";
export abstract class LiveSyncCommands {
plugin: ObsidianLiveSyncPlugin;
get app() {
@@ -31,13 +40,21 @@ export abstract class LiveSyncCommands {
abstract onunload(): void;
abstract onload(): void | Promise<void>;
$isMainReady() {
return this.plugin.$isMainReady();
_isMainReady() {
return this.plugin.$$isReady();
}
$isMainSuspended() {
return this.plugin.$isMainSuspended();
_isMainSuspended() {
return this.plugin.$$isSuspended();
}
$isDatabaseReady() {
return this.plugin.$isDatabaseReady();
_isDatabaseReady() {
return this.plugin.$$isDatabaseReady();
}
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {
if (typeof msg === "string" && level !== LOG_LEVEL_NOTICE) {
msg = `[${this.constructor.name}]\u{200A} ${msg}`;
}
// console.log(msg);
Logger(msg, level, key);
};
}

Submodule src/lib updated: 92d7b03916...e1688acb0f

View File

@@ -1,25 +1,40 @@
import { Plugin } from "./deps";
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, type HasSettings, type MetaEntry, type UXFileInfoStub, type MISSING_OR_ERROR, type AUTO_MERGED, } from "./lib/src/common/types.ts";
import {
type EntryDoc,
type LoadedEntry,
type ObsidianLiveSyncSettings,
type LOG_LEVEL,
type diff_result,
type DatabaseConnectingStatus,
type EntryHasPath,
type DocumentID,
type FilePathWithPrefix,
type FilePath,
LOG_LEVEL_INFO,
type HasSettings,
type MetaEntry,
type UXFileInfoStub,
type MISSING_OR_ERROR,
type AUTO_MERGED,
type RemoteDBSettings,
type TweakValues,
} from "./lib/src/common/types.ts";
import { type FileEventItem } from "./common/types.ts";
import { fireAndForget, type SimpleStore } from "./lib/src/common/utils.ts";
import { Logger } from "./lib/src/common/logger.ts";
import { cancelAllPeriodicTask, cancelAllTasks } from "./common/utils.ts";
import { versionNumberString2Number } from "./lib/src/string_and_binary/convert.ts";
import { type SimpleStore } from "./lib/src/common/utils.ts";
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB.ts";
import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator.js";
import {
LiveSyncAbstractReplicator,
type LiveSyncReplicatorEnv,
} from "./lib/src/replication/LiveSyncAbstractReplicator.js";
import { type KeyValueDatabase } from "./common/KeyValueDB.ts";
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
import { stopAllRunningProcessors } from "./lib/src/concurrency/processor.js";
import { reactiveSource, type ReactiveValue } from "./lib/src/dataobject/reactive.js";
import { type LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicator.js";
import { type LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator.js";
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes.js";
import { ObsHttpHandler } from "./modules/essentialObsidian/APILib/ObsHttpHandler.js";
import { $f, setLang } from "./lib/src/common/i18n.ts";
import { eventHub } from "./lib/src/hub/hub.ts";
import { EVENT_LAYOUT_READY, EVENT_PLUGIN_LOADED, EVENT_PLUGIN_UNLOADED, EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED } from "./common/events.ts";
import type { IObsidianModule } from "./modules/AbstractObsidianModule.ts";
import { ModuleDev } from "./modules/extras/ModuleDev.ts";
@@ -64,7 +79,8 @@ import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleRe
import { ModuleIntegratedTest } from "./modules/extras/ModuleIntegratedTest.ts";
import { ModuleRebuilder } from "./modules/core/ModuleRebuilder.ts";
import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts";
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts";
import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts";
function throwShouldBeOverridden(): never {
throw new Error("This function should be overridden by the module.");
@@ -83,34 +99,15 @@ const InterceptiveAny = Promise.resolve(undefined);
* All of above performed on injectModules function.
*/
export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv, LiveSyncJournalReplicatorEnv, LiveSyncCouchDBReplicatorEnv, HasSettings<ObsidianLiveSyncSettings> {
_log = Logger;
settings!: ObsidianLiveSyncSettings;
localDatabase!: LiveSyncLocalDB;
replicator!: LiveSyncAbstractReplicator;
_suspended = false;
get suspended() {
return this._suspended || !this.settings?.isConfigured;
}
set suspended(value: boolean) {
this._suspended = value;
}
get shouldBatchSave() {
return this.settings?.batchSave && this.settings?.liveSync != true;
}
get batchSaveMinimumDelay(): number {
return this.settings?.batchSaveMinimumDelay ?? DEFAULT_SETTINGS.batchSaveMinimumDelay
}
get batchSaveMaximumDelay(): number {
return this.settings?.batchSaveMaximumDelay ?? DEFAULT_SETTINGS.batchSaveMaximumDelay
}
deviceAndVaultName = "";
isReady = false;
packageVersion = "";
manifestVersion = "";
export default class ObsidianLiveSyncPlugin
extends Plugin
implements
LiveSyncLocalDBEnv,
LiveSyncReplicatorEnv,
LiveSyncJournalReplicatorEnv,
LiveSyncCouchDBReplicatorEnv,
HasSettings<ObsidianLiveSyncSettings>
{
// --> Module System
getAddOn<T extends LiveSyncCommands>(cls: string) {
for (const addon of this.addOns) {
@@ -123,6 +120,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo
addOns = [new ConfigSync(this), new HiddenFileSync(this)] as LiveSyncCommands[];
modules = [
new ModuleLiveSyncMain(this),
new ModuleExtraSyncObsidian(this, this),
// Only on Obsidian
new ModuleDatabaseFileAccess(this),
// Common
@@ -160,54 +159,89 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo
// Common modules
// Note: Platform-dependent functions are not entirely dependent on the core only, as they are from platform-dependent modules. Stubbing is sometimes required.
new ModuleCheckRemoteSize(this),
// Test and Dev Modules
// Test and Dev Modules
new ModuleDev(this, this),
new ModuleReplicateTest(this, this),
new ModuleIntegratedTest(this, this),
] as (IObsidianModule | AbstractModule)[];
injected = injectModules(this, [...this.modules, ...this.addOns] as ICoreModule[]);
// <-- Module System
$$addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void { throwShouldBeOverridden() }
$$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.
settings!: ObsidianLiveSyncSettings;
localDatabase!: LiveSyncLocalDB;
simpleStore!: SimpleStore<CheckPointInfo>;
replicator!: LiveSyncAbstractReplicator;
confirm!: Confirm;
storageAccess!: StorageAccess;
databaseFileAccess!: DatabaseFileAccess;
fileHandler!: ModuleFileHandler;
rebuilder!: Rebuilder;
// implementing interfaces
kvDB!: KeyValueDatabase;
last_successful_post = false;
getDatabase(): PouchDB.Database<EntryDoc> {
return this.localDatabase.localDatabase;
}
getSettings(): ObsidianLiveSyncSettings {
return this.settings;
}
totalFileEventCount = 0;
$$markFileListPossiblyChanged(): void {
throwShouldBeOverridden();
}
$$customFetchHandler(): ObsHttpHandler {
throw new Error("This function should be overridden by the module.");
throwShouldBeOverridden();
}
customFetchHandler() {
return this.$$customFetchHandler();
$$getLastPostFailedBySize(): boolean {
throwShouldBeOverridden();
}
getLastPostFailedBySize() {
return !this.last_successful_post;
$$isStorageInsensitive(): boolean {
throwShouldBeOverridden();
}
getDatabase(): PouchDB.Database<EntryDoc> { return this.localDatabase.localDatabase; }
getSettings(): ObsidianLiveSyncSettings { return this.settings; }
getIsMobile(): boolean { return this.isMobile; }
$$isStorageInsensitive(): boolean { throwShouldBeOverridden() }
get shouldCheckCaseInsensitive() {
if (this.$$isStorageInsensitive()) return false;
return !this.settings.handleFilenameCaseSensitive;
$$shouldCheckCaseInsensitive(): boolean {
throwShouldBeOverridden();
}
_unloaded = false;
$$isUnloaded(): boolean {
throwShouldBeOverridden();
}
requestCount = reactiveSource(0);
responseCount = reactiveSource(0);
@@ -222,9 +256,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo
processingFileEventCount = reactiveSource(0);
_totalProcessingCount?: ReactiveValue<number>;
get isReloadingScheduled() {
return this._totalProcessingCount !== undefined;
}
replicationStat = reactiveSource({
sent: 0,
@@ -233,73 +264,104 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo
maxPushSeq: 0,
lastSyncPullSeq: 0,
lastSyncPushSeq: 0,
syncStatus: "CLOSED" as DatabaseConnectingStatus
syncStatus: "CLOSED" as DatabaseConnectingStatus,
});
get isMobile() { return this.$$isMobile(); }
// Plug-in's overrideable functions
onload() { void this.onLiveSyncLoad(); }
async saveSettings() { await this.$$saveSettingData(); }
onunload() { return void this.onLiveSyncUnload(); }
// <-- Plug-in's overrideable functions
$$connectRemoteCouchDB(uri: string, auth: {
username: string;
password: string
}, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean): Promise<string | {
db: PouchDB.Database<EntryDoc>;
info: PouchDB.Core.DatabaseInfo
}> {
$$isReloadingScheduled(): boolean {
throwShouldBeOverridden();
}
$$getReplicator(): LiveSyncAbstractReplicator {
throwShouldBeOverridden();
}
$$connectRemoteCouchDB(
uri: string,
auth: {
username: string;
password: string;
},
disableRequestURI: boolean,
passphrase: string | false,
useDynamicIterationCount: boolean,
performSetup: boolean,
skipInfo: boolean,
compression: boolean
): Promise<
| string
| {
db: PouchDB.Database<EntryDoc>;
info: PouchDB.Core.DatabaseInfo;
}
> {
throwShouldBeOverridden();
}
$$isMobile(): boolean { throwShouldBeOverridden(); }
$$vaultName(): string { throwShouldBeOverridden(); }
$$isMobile(): boolean {
throwShouldBeOverridden();
}
$$vaultName(): string {
throwShouldBeOverridden();
}
// --> Path
$$getActiveFilePath(): FilePathWithPrefix | undefined { throwShouldBeOverridden(); }
$$getActiveFilePath(): FilePathWithPrefix | undefined {
throwShouldBeOverridden();
}
// <-- Path
// --> Path conversion
$$id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix { throwShouldBeOverridden(); }
$$id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
throwShouldBeOverridden();
}
$$path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> { 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(); }
$$createPouchDBInstance<T extends object>(
name?: string,
options?: PouchDB.Configuration.DatabaseConfiguration
): PouchDB.Database<T> {
throwShouldBeOverridden();
}
$allOnDBUnload(db: LiveSyncLocalDB): void { return; }
$allOnDBClose(db: LiveSyncLocalDB): void { return; }
$allOnDBUnload(db: LiveSyncLocalDB): void {
return;
}
$allOnDBClose(db: LiveSyncLocalDB): void {
return;
}
// <!-- Database
$anyNewReplicator(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): Promise<LiveSyncAbstractReplicator> { throwShouldBeOverridden(); }
$anyNewReplicator(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): Promise<LiveSyncAbstractReplicator> {
throwShouldBeOverridden();
}
$everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> { return InterceptiveEvery; }
$everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return InterceptiveEvery;
}
$everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> { return InterceptiveEvery; }
getReplicator() {
return this.replicator;
$everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return InterceptiveEvery;
}
// end interfaces
$$getVaultName(): string { throwShouldBeOverridden(); }
$$getVaultName(): string {
throwShouldBeOverridden();
}
simpleStore!: SimpleStore<CheckPointInfo>
$$getSimpleStore<T>(kind: string): SimpleStore<T> { throwShouldBeOverridden(); }
$$getSimpleStore<T>(kind: string): SimpleStore<T> {
throwShouldBeOverridden();
}
// trench!: Trench;
// --> Events
/*
@@ -339,185 +401,114 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo
*/
$everyOnLayoutReady(): Promise<boolean> { return InterceptiveEvery }
$everyOnFirstInitialize(): Promise<boolean> { return InterceptiveEvery }
$everyOnLayoutReady(): Promise<boolean> {
return InterceptiveEvery;
}
$everyOnFirstInitialize(): Promise<boolean> {
return InterceptiveEvery;
}
// Some Module should call this function to start the plugin.
async onLiveSyncReady() {
if (!await this.$everyOnLayoutReady()) return;
eventHub.emitEvent(EVENT_LAYOUT_READY);
if (this.settings.suspendFileWatching) {
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL_NOTICE);
}
if (this.settings.suspendParseReplicationResult) {
Logger("'Suspend database reflecting' turned on. Are you sure this is what you intended? Every replicated change will be postponed until disabling this option.", LOG_LEVEL_NOTICE);
}
const isInitialized = await this.$$initializeDatabase(false, false);
if (!isInitialized) {
//TODO:stop all sync.
return false;
}
if (!await this.$everyOnFirstInitialize()) return;
await this.realizeSettingSyncMode();
fireAndForget(async () => {
Logger(`Additional safety scan..`, LOG_LEVEL_VERBOSE);
if (!await this.$allScanStat()) {
Logger(`Additional safety scan has been failed on some module`, LOG_LEVEL_NOTICE);
} else {
Logger(`Additional safety scan done`, LOG_LEVEL_VERBOSE);
}
});
$$onLiveSyncReady(): Promise<false | undefined> {
throwShouldBeOverridden();
}
wireUpEvents() {
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
const settings = evt.detail;
this.localDatabase.settings = settings;
setLang(settings.displayLanguage);
eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
});
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
fireAndForget(() => this.realizeSettingSyncMode());
})
$$wireUpEvents(): void {
throwShouldBeOverridden();
}
$$onLiveSyncLoad(): Promise<void> {
throwShouldBeOverridden();
}
async onLiveSyncLoad() {
this.wireUpEvents();
// debugger;
eventHub.emitEvent(EVENT_PLUGIN_LOADED, this);
Logger("loading plugin");
if (!await this.$everyOnloadStart()) {
Logger("Plugin initialising has been cancelled by some module", LOG_LEVEL_NOTICE);
return;
}
// this.addUIs();
//@ts-ignore
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
//@ts-ignore
const packageVersion: string = PACKAGE_VERSION || "0.0.0";
this.manifestVersion = manifestVersion;
this.packageVersion = packageVersion;
Logger($f`Self-hosted LiveSync${" v"}${manifestVersion} ${packageVersion}`);
await this.$$loadSettings();
if (!await this.$everyOnloadAfterLoadSettings()) {
Logger("Plugin initialising has been cancelled by some module", LOG_LEVEL_NOTICE);
return;
}
const lsKey = "obsidian-live-sync-ver" + this.$$getVaultName();
const last_version = localStorage.getItem(lsKey);
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
if (lastVersion > this.settings.lastReadUpdates && this.settings.isConfigured) {
Logger($f`Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.`, LOG_LEVEL_NOTICE);
}
//@ts-ignore
if (this.isMobile) {
this.settings.disableRequestURI = true;
}
if (last_version && Number(last_version) < VER) {
this.settings.liveSync = false;
this.settings.syncOnSave = false;
this.settings.syncOnEditorSave = false;
this.settings.syncOnStart = false;
this.settings.syncOnFileOpen = false;
this.settings.syncAfterMerge = false;
this.settings.periodicReplication = false;
this.settings.versionUpFlash = $f`Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.`;
await this.saveSettings();
}
localStorage.setItem(lsKey, `${VER}`);
await this.$$openDatabase();
this.realizeSettingSyncMode = this.realizeSettingSyncMode.bind(this);
// this.$$parseReplicationResult = this.$$parseReplicationResult.bind(this);
// this.$$replicate = this.$$replicate.bind(this);
this.onLiveSyncReady = this.onLiveSyncReady.bind(this);
await this.$everyOnload();
await Promise.all(this.addOns.map(e => e.onload()));
}
async onLiveSyncUnload() {
eventHub.emitEvent(EVENT_PLUGIN_UNLOADED);
await this.$allStartOnUnload();
cancelAllPeriodicTask();
cancelAllTasks();
stopAllRunningProcessors();
await this.$allOnUnload();
this._unloaded = true;
for (const addOn of this.addOns) {
addOn.onunload();
}
if (this.localDatabase != null) {
this.localDatabase.onunload();
if (this.replicator) {
this.replicator?.closeReplication();
}
await this.localDatabase.close();
}
Logger($f`unloading plugin`);
$$onLiveSyncUnload(): Promise<void> {
throwShouldBeOverridden();
}
$allScanStat(): Promise<boolean> {
return InterceptiveAll;
}
$everyOnloadStart(): Promise<boolean> { return InterceptiveEvery; }
$everyOnloadAfterLoadSettings(): Promise<boolean> { return InterceptiveEvery; }
$everyOnload(): Promise<boolean> { return InterceptiveEvery; }
$anyHandlerProcessesFileEvent(item: FileEventItem): Promise<boolean | undefined> { return InterceptiveAny; }
$allStartOnUnload(): Promise<boolean> { return InterceptiveAll }
$allOnUnload(): Promise<boolean> { return InterceptiveAll; }
$$openDatabase(): Promise<boolean> { throwShouldBeOverridden() }
async realizeSettingSyncMode() {
await this.$everyBeforeSuspendProcess();
await this.$everyBeforeRealizeSetting();
this.localDatabase.refreshSettings();
await this.$everyCommitPendingFileEvent();
await this.$everyRealizeSettingSyncMode();
// disable all sync temporary.
if (this.suspended) return;
await this.$everyOnResumeProcess();
await this.$everyAfterResumeProcess();
await this.$everyAfterRealizeSetting();
$everyOnloadStart(): Promise<boolean> {
return InterceptiveEvery;
}
$$performRestart() { throwShouldBeOverridden(); }
$$clearUsedPassphrase(): void { throwShouldBeOverridden() }
$$loadSettings(): Promise<void> { throwShouldBeOverridden() }
$everyOnloadAfterLoadSettings(): Promise<boolean> {
return InterceptiveEvery;
}
$$saveDeviceAndVaultName(): void { throwShouldBeOverridden(); }
$everyOnload(): Promise<boolean> {
return InterceptiveEvery;
}
$$saveSettingData(): Promise<void> { throwShouldBeOverridden() }
$anyHandlerProcessesFileEvent(item: FileEventItem): Promise<boolean | undefined> {
return InterceptiveAny;
}
$anyProcessOptionalFileEvent(path: FilePath): Promise<boolean | undefined> { return InterceptiveAny; }
$allStartOnUnload(): Promise<boolean> {
return InterceptiveAll;
}
$allOnUnload(): Promise<boolean> {
return InterceptiveAll;
}
$everyCommitPendingFileEvent(): Promise<boolean> { return InterceptiveEvery }
$$openDatabase(): Promise<boolean> {
throwShouldBeOverridden();
}
// ->
$anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | undefined | "newer"> { return InterceptiveAny; }
$$realizeSettingSyncMode(): Promise<void> {
throwShouldBeOverridden();
}
$$performRestart() {
throwShouldBeOverridden();
}
$$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> { throwShouldBeOverridden() }
$$clearUsedPassphrase(): void {
throwShouldBeOverridden();
}
$$loadSettings(): Promise<void> {
throwShouldBeOverridden();
}
$$queueConflictCheck(file: FilePathWithPrefix): Promise<void> { throwShouldBeOverridden() }
$$saveDeviceAndVaultName(): void {
throwShouldBeOverridden();
}
$$waitForAllConflictProcessed(): Promise<boolean> { throwShouldBeOverridden() }
$$saveSettingData(): Promise<void> {
throwShouldBeOverridden();
}
$anyProcessOptionalFileEvent(path: FilePath): Promise<boolean | undefined> {
return InterceptiveAny;
}
$everyCommitPendingFileEvent(): Promise<boolean> {
return InterceptiveEvery;
}
// ->
$anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | undefined | "newer"> {
return InterceptiveAny;
}
$$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
throwShouldBeOverridden();
}
$$queueConflictCheck(file: FilePathWithPrefix): Promise<void> {
throwShouldBeOverridden();
}
$$waitForAllConflictProcessed(): Promise<boolean> {
throwShouldBeOverridden();
}
//<-- Conflict Check
$anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise<boolean | undefined> { return InterceptiveAny; }
$anyProcessReplicatedDoc(doc: MetaEntry): Promise<boolean | undefined> { return InterceptiveAny; }
$anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise<boolean | undefined> {
return InterceptiveAny;
}
$anyProcessReplicatedDoc(doc: MetaEntry): Promise<boolean | undefined> {
return InterceptiveAny;
}
//---> Sync
$$parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
@@ -547,85 +538,169 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo
return InterceptiveEvery;
}
$$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
throwShouldBeOverridden();
}
$$checkAndAskUseRemoteConfiguration(
settings: RemoteDBSettings
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
throwShouldBeOverridden();
}
$$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> { throwShouldBeOverridden(); }
$everyBeforeReplicate(showMessage: boolean): Promise<boolean> { return InterceptiveEvery; }
$$replicate(showMessage: boolean = false): Promise<boolean | void> { throwShouldBeOverridden() }
$everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
return InterceptiveEvery;
}
$$replicate(showMessage: boolean = false): Promise<boolean | void> {
throwShouldBeOverridden();
}
$everyOnDatabaseInitialized(showingNotice: boolean): Promise<boolean> { throwShouldBeOverridden() }
$everyOnDatabaseInitialized(showingNotice: boolean): Promise<boolean> {
throwShouldBeOverridden();
}
$$initializeDatabase(showingNotice: boolean = false, reopenDatabase = true): Promise<boolean> { throwShouldBeOverridden() }
$$initializeDatabase(showingNotice: boolean = false, reopenDatabase = true): Promise<boolean> {
throwShouldBeOverridden();
}
$anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> { return InterceptiveAny; }
$anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
return InterceptiveAny;
}
$$replicateAllToServer(showingNotice: boolean = false): Promise<boolean> { throwShouldBeOverridden() }
$$replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> { throwShouldBeOverridden() }
$$replicateAllToServer(
showingNotice: boolean = false,
sendChunksInBulkDisabled: boolean = false
): Promise<boolean> {
throwShouldBeOverridden();
}
$$replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
throwShouldBeOverridden();
}
// Remote Governing
$$markRemoteLocked(lockByClean: boolean = false): Promise<void> { throwShouldBeOverridden() }
$$markRemoteLocked(lockByClean: boolean = false): Promise<void> {
throwShouldBeOverridden();
}
$$markRemoteUnlocked(): Promise<void> { throwShouldBeOverridden() }
$$markRemoteUnlocked(): Promise<void> {
throwShouldBeOverridden();
}
$$markRemoteResolved(): Promise<void> { throwShouldBeOverridden() }
$$markRemoteResolved(): Promise<void> {
throwShouldBeOverridden();
}
// <-- Remote Governing
$$isFileSizeExceeded(size: number): boolean { throwShouldBeOverridden() }
$$performFullScan(showingNotice?: boolean): Promise<void> { throwShouldBeOverridden(); }
$anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean | undefined> { return InterceptiveAny; }
$$resolveConflictByDeletingRev(path: FilePathWithPrefix, deleteRevision: string, subTitle = ""): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> { throwShouldBeOverridden(); }
$$resolveConflict(filename: FilePathWithPrefix): Promise<void> { throwShouldBeOverridden(); }
$anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> { throwShouldBeOverridden(); }
$$waitForReplicationOnce(): Promise<boolean | void> { throwShouldBeOverridden(); }
$$resetLocalDatabase(): Promise<void> { throwShouldBeOverridden(); }
$$tryResetRemoteDatabase(): Promise<void> { throwShouldBeOverridden(); }
$$tryCreateRemoteDatabase(): Promise<void> { throwShouldBeOverridden(); }
$$isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> { throwShouldBeOverridden(); }
$$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false): Promise<boolean> { throwShouldBeOverridden(); }
$$askReload(message?: string) { throwShouldBeOverridden(); }
$$scheduleAppReload() { throwShouldBeOverridden(); }
//--- Setup
$allSuspendAllSync(): Promise<boolean> { return InterceptiveAll; }
$allSuspendExtraSync(): Promise<boolean> { return InterceptiveAll; }
$allAskUsingOptionalSyncFeature(opt: {
enableFetch?: boolean,
enableOverwrite?: boolean
}): Promise<boolean> {
$$isFileSizeExceeded(size: number): boolean {
throwShouldBeOverridden();
}
$anyConfigureOptionalSyncFeature(mode: string): Promise<void> { throwShouldBeOverridden(); }
$$showView(viewType: string): Promise<void> { throwShouldBeOverridden(); }
$$performFullScan(showingNotice?: boolean): Promise<void> {
throwShouldBeOverridden();
}
$anyResolveConflictByUI(
filename: FilePathWithPrefix,
conflictCheckResult: diff_result
): Promise<boolean | undefined> {
return InterceptiveAny;
}
$$resolveConflictByDeletingRev(
path: FilePathWithPrefix,
deleteRevision: string,
subTitle = ""
): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> {
throwShouldBeOverridden();
}
$$resolveConflict(filename: FilePathWithPrefix): Promise<void> {
throwShouldBeOverridden();
}
$anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
throwShouldBeOverridden();
}
$$waitForReplicationOnce(): Promise<boolean | void> {
throwShouldBeOverridden();
}
$$resetLocalDatabase(): Promise<void> {
throwShouldBeOverridden();
}
$$tryResetRemoteDatabase(): Promise<void> {
throwShouldBeOverridden();
}
$$tryCreateRemoteDatabase(): Promise<void> {
throwShouldBeOverridden();
}
$$isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> {
throwShouldBeOverridden();
}
$$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false): Promise<boolean> {
throwShouldBeOverridden();
}
$$askReload(message?: string) {
throwShouldBeOverridden();
}
$$scheduleAppReload() {
throwShouldBeOverridden();
}
//--- Setup
$allSuspendAllSync(): Promise<boolean> {
return InterceptiveAll;
}
$allSuspendExtraSync(): Promise<boolean> {
return InterceptiveAll;
}
$allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }): Promise<boolean> {
throwShouldBeOverridden();
}
$anyConfigureOptionalSyncFeature(mode: string): Promise<void> {
throwShouldBeOverridden();
}
$$showView(viewType: string): Promise<void> {
throwShouldBeOverridden();
}
// For Development: Ensure reliability MORE AND MORE. May the this plug-in helps all of us.
$everyModuleTest(): Promise<boolean> { return InterceptiveEvery; }
$everyModuleTestMultiDevice(): Promise<boolean> { return InterceptiveEvery; }
$$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void { throwShouldBeOverridden(); }
$everyModuleTest(): Promise<boolean> {
return InterceptiveEvery;
}
$everyModuleTestMultiDevice(): Promise<boolean> {
return InterceptiveEvery;
}
$$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void {
throwShouldBeOverridden();
}
$isMainReady(): boolean { return this.isReady; }
$isMainSuspended(): boolean { return this.suspended; }
$isThisModuleEnabled(): boolean { return true; }
$isDatabaseReady(): boolean { return this.localDatabase.isReady; }
_isThisModuleEnabled(): boolean {
return true;
}
$anyGetAppId(): Promise<string | undefined> { return InterceptiveAny; }
$anyGetAppId(): Promise<string | undefined> {
return InterceptiveAny;
}
// Plug-in's overrideable functions
onload() {
void this.$$onLiveSyncLoad();
}
async saveSettings() {
await this.$$saveSettingData();
}
onunload() {
return void this.$$onLiveSyncUnload();
}
// <-- Plug-in's overrideable functions
}
// For now,
export type LiveSyncCore = ObsidianLiveSyncPlugin;
export type LiveSyncCore = ObsidianLiveSyncPlugin;

View File

@@ -3,8 +3,14 @@ import type { LOG_LEVEL } from "../lib/src/common/types";
import type { LiveSyncCore } from "../main";
import { unique } from "octagonal-wheels/collection";
import type { IObsidianModule } from "./AbstractObsidianModule.ts";
import type { ICoreModuleBase, AllInjectableProps, AllExecuteProps, EveryExecuteProps, AnyExecuteProps, ICoreModule } from "./ModuleTypes";
import type {
ICoreModuleBase,
AllInjectableProps,
AllExecuteProps,
EveryExecuteProps,
AnyExecuteProps,
ICoreModule,
} from "./ModuleTypes";
function isOverridableKey(key: string): key is keyof ICoreModuleBase {
return key.startsWith("$");
@@ -14,7 +20,6 @@ function isInjectableKey(key: string): key is keyof AllInjectableProps {
return key.startsWith("$$");
}
function isAllExecuteKey(key: string): key is keyof AllExecuteProps {
return key.startsWith("$all");
}
@@ -35,15 +40,17 @@ function isAnyExecuteKey(key: string): key is keyof AnyExecuteProps {
* All of above performed on injectModules function.
*/
export function injectModules<T extends ICoreModule>(target: T, modules: ICoreModule[]) {
const allKeys = unique([...Object.keys(Object.getOwnPropertyDescriptors(target)),
...Object.keys(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(target)))]).filter(e => e.startsWith("$")) as (keyof ICoreModule)[];
const allKeys = unique([
...Object.keys(Object.getOwnPropertyDescriptors(target)),
...Object.keys(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(target))),
]).filter((e) => e.startsWith("$")) as (keyof ICoreModule)[];
const moduleMap = new Map<string, IObsidianModule[]>();
for (const module of modules) {
for (const key of allKeys) {
if (isOverridableKey(key)) {
if (key in module) {
const list = moduleMap.get(key) || [];
if (typeof module[key] === 'function') {
if (typeof module[key] === "function") {
module[key] = module[key].bind(module) as any;
}
list.push(module);
@@ -74,7 +81,7 @@ export function injectModules<T extends ICoreModule>(target: T, modules: ICoreMo
}
}
return true;
}
};
for (const module of modules) {
Logger(`[${module.constructor.name}]: Injected (All) ${key} `, LOG_LEVEL_VERBOSE);
}
@@ -94,7 +101,7 @@ export function injectModules<T extends ICoreModule>(target: T, modules: ICoreMo
}
}
return true;
}
};
for (const module of modules) {
Logger(`[${module.constructor.name}]: Injected (Every) ${key} `, LOG_LEVEL_VERBOSE);
}
@@ -115,7 +122,7 @@ export function injectModules<T extends ICoreModule>(target: T, modules: ICoreMo
}
}
return false;
}
};
for (const module of modules) {
Logger(`[${module.constructor.name}]: Injected (Any) ${key} `, LOG_LEVEL_VERBOSE);
}
@@ -127,7 +134,6 @@ export function injectModules<T extends ICoreModule>(target: T, modules: ICoreMo
return true;
}
export abstract class AbstractModule {
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {
if (typeof msg === "string" && level !== LOG_LEVEL_NOTICE) {
@@ -173,11 +179,10 @@ export abstract class AbstractModule {
return this.testFail(`${key} failed: ${ret}`);
}
this.addTestResult(key, true, "");
}
catch (ex: any) {
} catch (ex: any) {
this.addTestResult(key, false, "Failed by Exception", ex.toString());
return this.testFail(`${key} failed: ${ex}`);
}
return this.testDone();
}
}
}

View File

@@ -4,15 +4,12 @@ import type ObsidianLiveSyncPlugin from "../main";
import { AbstractModule } from "./AbstractModule.ts";
import type { ChainableExecuteFunction, OverridableFunctionsKeys } from "./ModuleTypes";
export type IObsidianModuleBase = OverridableFunctionsKeys<ObsidianLiveSyncPlugin>;
export type IObsidianModule = Prettify<Partial<IObsidianModuleBase>>
export type IObsidianModule = Prettify<Partial<IObsidianModuleBase>>;
export type ModuleKeys = keyof IObsidianModule;
export type ChainableModuleProps = ChainableExecuteFunction<ObsidianLiveSyncPlugin>;
export abstract class AbstractObsidianModule extends AbstractModule {
addCommand = this.plugin.addCommand.bind(this.plugin);
registerView = this.plugin.registerView.bind(this.plugin);
addRibbonIcon = this.plugin.addRibbonIcon.bind(this.plugin);
@@ -31,25 +28,27 @@ export abstract class AbstractObsidianModule extends AbstractModule {
return this.plugin.app;
}
constructor(public plugin: ObsidianLiveSyncPlugin, public core: LiveSyncCore) {
constructor(
public plugin: ObsidianLiveSyncPlugin,
public core: LiveSyncCore
) {
super(core);
}
saveSettings = this.plugin.saveSettings.bind(this.plugin);
$isMainReady() {
return this.core.$isMainReady();
_isMainReady() {
return this.core.$$isReady();
}
$isMainSuspended() {
return this.core.$isMainSuspended();
_isMainSuspended() {
return this.core.$$isSuspended();
}
$isDatabaseReady() {
return this.core.$isDatabaseReady();
_isDatabaseReady() {
return this.core.$$isDatabaseReady();
}
//should be overridden
$isThisModuleEnabled() {
return true
_isThisModuleEnabled() {
return true;
}
}
}

View File

@@ -1,38 +1,53 @@
import type { Prettify } from "../lib/src/common/types";
import type { LiveSyncCore } from "../main";
export type OverridableFunctionsKeys<T> = {
[K in keyof T as K extends `$${string}` ? K : never]: T[K];
}
};
export type ChainableExecuteFunction<T> = {
[K in keyof T as K extends `$${string}` ? (T[K] extends (...args: any) => ChainableFunctionResult ? K : never) : never]: T[K];
}
[K in keyof T as K extends `$${string}`
? T[K] extends (...args: any) => ChainableFunctionResult
? K
: never
: never]: T[K];
};
export type ICoreModuleBase = OverridableFunctionsKeys<LiveSyncCore>;
export type ICoreModule = Prettify<Partial<ICoreModuleBase>>
export type ICoreModule = Prettify<Partial<ICoreModuleBase>>;
export type CoreModuleKeys = keyof ICoreModule;
export type ChainableFunctionResult =
Promise<boolean | undefined | string>
| Promise<boolean | undefined | string>
| Promise<boolean | undefined>
| Promise<boolean>
| Promise<void>;
export type ChainableFunctionResultOrAll = Promise<boolean | undefined | string | void>;
type AllExecuteFunction<T> = {
[K in keyof T as K extends `$all${string}` ? (T[K] extends (...args: any[]) => ChainableFunctionResultOrAll ? K : never) : never]: T[K];
}
[K in keyof T as K extends `$all${string}`
? T[K] extends (...args: any[]) => ChainableFunctionResultOrAll
? K
: never
: never]: T[K];
};
type EveryExecuteFunction<T> = {
[K in keyof T as K extends `$every${string}` ? (T[K] extends (...args: any[]) => ChainableFunctionResult ? K : never) : never]: T[K];
}
[K in keyof T as K extends `$every${string}`
? T[K] extends (...args: any[]) => ChainableFunctionResult
? K
: never
: never]: T[K];
};
type AnyExecuteFunction<T> = {
[K in keyof T as K extends `$any${string}` ? (T[K] extends (...args: any[]) => ChainableFunctionResult ? K : never) : never]: T[K];
}
[K in keyof T as K extends `$any${string}`
? T[K] extends (...args: any[]) => ChainableFunctionResult
? K
: never
: never]: T[K];
};
type InjectableFunction<T> = {
[K in keyof T as K extends `$$${string}` ? (T[K] extends (...args: any[]) => any ? K : never) : never]: T[K];
}
};
export type AllExecuteProps = AllExecuteFunction<LiveSyncCore>;
export type EveryExecuteProps = EveryExecuteFunction<LiveSyncCore>;
export type AnyExecuteProps = AnyExecuteFunction<LiveSyncCore>;

View File

@@ -1,15 +1,29 @@
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { EVENT_FILE_SAVED, eventHub } from "../../common/events";
import { getPathFromUXFileInfo, isInternalMetadata, markChangesAreSame } from "../../common/utils";
import type { UXFileInfoStub, FilePathWithPrefix, UXFileInfo, MetaEntry, LoadedEntry, FilePath, SavingEntry } from "../../lib/src/common/types";
import type {
UXFileInfoStub,
FilePathWithPrefix,
UXFileInfo,
MetaEntry,
LoadedEntry,
FilePath,
SavingEntry,
} from "../../lib/src/common/types";
import type { DatabaseFileAccess } from "../interfaces/DatabaseFileAccess";
import { type IObsidianModule } from "../AbstractObsidianModule.ts";
import { isPlainText, shouldBeIgnored } from "../../lib/src/string_and_binary/path";
import { createBlob, createTextBlob, delay, determineTypeFromBlob, isDocContentSame, readContent } from "../../lib/src/common/utils";
import {
createBlob,
createTextBlob,
delay,
determineTypeFromBlob,
isDocContentSame,
readContent,
} from "../../lib/src/common/utils";
import { serialized } from "octagonal-wheels/concurrency/lock";
import { AbstractModule } from "../AbstractModule.ts";
export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidianModule, DatabaseFileAccess {
$everyOnload(): Promise<boolean> {
this.core.databaseFileAccess = this;
@@ -18,7 +32,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
async $everyModuleTest(): Promise<boolean> {
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.
const conflicts = await this.getConflictedRevs("autoTest.md" as FilePathWithPrefix);
for (const rev of conflicts) {
@@ -27,16 +41,21 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
await this.delete("autoTest.md" as FilePathWithPrefix);
// OK, begin!
await this._test("storeContent", async () => (await this.storeContent("autoTest.md" as FilePathWithPrefix, testString)));
await this._test(
"storeContent",
async () => await this.storeContent("autoTest.md" as FilePathWithPrefix, testString)
);
// For test, we need to clear the caches.
await this.localDatabase.hashCaches.clear();
await this._test("readContent", async () => {
const content = await this.fetch("autoTest.md" as FilePathWithPrefix);
if (!content) return "File not found";
if (content.deleted) return "File is deleted";
return (await content.body.text() == testString) ? true : `Content is not same ${await content.body.text()}`;
return (await content.body.text()) == testString
? true
: `Content is not same ${await content.body.text()}`;
});
await this._test("delete", async () => (await this.delete("autoTest.md" as FilePathWithPrefix)));
await this._test("delete", async () => await this.delete("autoTest.md" as FilePathWithPrefix));
await this._test("read deleted content", async () => {
const content = await this.fetch("autoTest.md" as FilePathWithPrefix);
if (!content) return true;
@@ -49,7 +68,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
async checkIsTargetFile(file: UXFileInfoStub | FilePathWithPrefix): Promise<boolean> {
const path = getPathFromUXFileInfo(file);
if (!await this.core.$$isTargetFile(path)) {
if (!(await this.core.$$isTargetFile(path))) {
this._log(`File is not target`, LOG_LEVEL_VERBOSE);
return false;
}
@@ -61,7 +80,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
}
async delete(file: UXFileInfoStub | FilePathWithPrefix, rev?: string): Promise<boolean> {
if (!await this.checkIsTargetFile(file)) {
if (!(await this.checkIsTargetFile(file))) {
return true;
}
const fullPath = getPathFromUXFileInfo(file);
@@ -95,13 +114,18 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
type: "file",
},
body: blob,
}
};
return await this._store(dummyUXFileInfo, true, false, false);
}
async _store(file: UXFileInfo, force: boolean = false, skipCheck?: boolean, onlyChunks?: boolean): Promise<boolean> {
async _store(
file: UXFileInfo,
force: boolean = false,
skipCheck?: boolean,
onlyChunks?: boolean
): Promise<boolean> {
if (!skipCheck) {
if (!await this.checkIsTargetFile(file)) {
if (!(await this.checkIsTargetFile(file))) {
return true;
}
}
@@ -133,6 +157,10 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
//upsert should locked
const msg = `STORAGE -> DB (${datatype}) `;
const isNotChanged = await serialized("file-" + fullPath, async () => {
if (force) {
this._log(msg + "Force writing " + fullPath, LOG_LEVEL_VERBOSE);
return false;
}
// Commented out temporarily: this checks that the file was made ourself.
// if (this.core.storageAccess.recentlyTouched(file)) {
// return true;
@@ -143,17 +171,33 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
const oldData = { data: old.data, deleted: old._deleted || old.deleted };
const newData = { data: d.data, deleted: d._deleted || d.deleted };
if (oldData.deleted != newData.deleted) return false;
if (!await isDocContentSame(old.data, newData.data)) return false;
this._log(msg + "Skipped (not changed) " + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE);
if (!(await isDocContentSame(old.data, newData.data))) return false;
this._log(
msg + "Skipped (not changed) " + fullPath + (d._deleted || d.deleted ? " (deleted)" : ""),
LOG_LEVEL_VERBOSE
);
markChangesAreSame(old, d.mtime, old.mtime);
return true;
// d._rev = old._rev;
}
} catch (ex) {
if (force) {
this._log(msg + "Error, Could not check the diff for the old one." + (force ? "force writing." : "") + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE);
this._log(
msg +
"Error, Could not check the diff for the old one." +
(force ? "force writing." : "") +
fullPath +
(d._deleted || d.deleted ? " (deleted)" : ""),
LOG_LEVEL_VERBOSE
);
} else {
this._log(msg + "Error, Could not check the diff for the old one." + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE);
this._log(
msg +
"Error, Could not check the diff for the old one." +
fullPath +
(d._deleted || d.deleted ? " (deleted)" : ""),
LOG_LEVEL_VERBOSE
);
}
this._log(ex, LOG_LEVEL_VERBOSE);
return !force;
@@ -173,7 +217,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
}
async getConflictedRevs(file: UXFileInfoStub | FilePathWithPrefix): Promise<string[]> {
if (!await this.checkIsTargetFile(file)) {
if (!(await this.checkIsTargetFile(file))) {
return [];
}
const filename = getPathFromUXFileInfo(file);
@@ -184,9 +228,13 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
return doc._conflicts || [];
}
async fetch(file: UXFileInfoStub | FilePathWithPrefix,
rev?: string, waitForReady?: boolean, skipCheck = false): Promise<UXFileInfo | false> {
if (skipCheck && !await this.checkIsTargetFile(file)) {
async fetch(
file: UXFileInfoStub | FilePathWithPrefix,
rev?: string,
waitForReady?: boolean,
skipCheck = false
): Promise<UXFileInfo | false> {
if (skipCheck && !(await this.checkIsTargetFile(file))) {
return false;
}
@@ -206,39 +254,55 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
},
body: data,
deleted: entry.deleted || entry._deleted,
}
};
if (isInternalMetadata(entry.path)) {
fileInfo.isInternal = true;
}
return fileInfo;
}
async fetchEntryMeta(file: UXFileInfoStub | FilePathWithPrefix,
rev?: string, skipCheck = false): Promise<MetaEntry | false> {
async fetchEntryMeta(
file: UXFileInfoStub | FilePathWithPrefix,
rev?: string,
skipCheck = false
): Promise<MetaEntry | false> {
const filename = getPathFromUXFileInfo(file);
if (skipCheck && !await this.checkIsTargetFile(file)) {
if (skipCheck && !(await this.checkIsTargetFile(file))) {
return false;
}
const doc = await this.localDatabase.getDBEntryMeta(
filename, rev ? { rev: rev } : undefined, true);
const doc = await this.localDatabase.getDBEntryMeta(filename, rev ? { rev: rev } : undefined, true);
if (doc === false) {
return false;
}
return doc as MetaEntry;
}
async fetchEntryFromMeta(meta: MetaEntry, waitForReady: boolean = true, skipCheck = false): Promise<LoadedEntry | false> {
if (skipCheck && !await this.checkIsTargetFile(meta.path)) {
async fetchEntryFromMeta(
meta: MetaEntry,
waitForReady: boolean = true,
skipCheck = false
): Promise<LoadedEntry | false> {
if (skipCheck && !(await this.checkIsTargetFile(meta.path))) {
return false;
}
const doc = await this.localDatabase.getDBEntryFromMeta(meta as LoadedEntry, undefined, false, waitForReady, true);
const doc = await this.localDatabase.getDBEntryFromMeta(
meta as LoadedEntry,
undefined,
false,
waitForReady,
true
);
if (doc === false) {
return false;
}
return doc;
}
async fetchEntry(file: UXFileInfoStub | FilePathWithPrefix,
rev?: string, waitForReady: boolean = true, skipCheck = false): Promise<LoadedEntry | false> {
if (skipCheck && !await this.checkIsTargetFile(file)) {
async fetchEntry(
file: UXFileInfoStub | FilePathWithPrefix,
rev?: string,
waitForReady: boolean = true,
skipCheck = false
): Promise<LoadedEntry | false> {
if (skipCheck && !(await this.checkIsTargetFile(file))) {
return false;
}
const entry = await this.fetchEntryMeta(file, rev, true);
@@ -249,7 +313,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
return doc;
}
async deleteFromDBbyPath(fullPath: FilePath | FilePathWithPrefix, rev?: string): Promise<boolean> {
if (!await this.checkIsTargetFile(fullPath)) {
if (!(await this.checkIsTargetFile(fullPath))) {
this._log(`storeFromStorage: File is not target: ${fullPath}`);
return true;
}
@@ -258,5 +322,4 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia
eventHub.emitEvent(EVENT_FILE_SAVED);
return ret;
}
}
}

View File

@@ -1,16 +1,29 @@
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { serialized } from "octagonal-wheels/concurrency/lock";
import type { FileEventItem } from "../../common/types";
import type { FilePath, FilePathWithPrefix, MetaEntry, UXFileInfo, UXFileInfoStub, UXInternalFileInfoStub } from "../../lib/src/common/types";
import type {
FilePath,
FilePathWithPrefix,
MetaEntry,
UXFileInfo,
UXFileInfoStub,
UXInternalFileInfoStub,
} from "../../lib/src/common/types";
import { AbstractModule } from "../AbstractModule.ts";
import { compareFileFreshness, EVEN, getPath, getPathFromUXFileInfo, getPathWithoutPrefix, markChangesAreSame } from "../../common/utils";
import {
compareFileFreshness,
EVEN,
getPath,
getPathFromUXFileInfo,
getPathWithoutPrefix,
markChangesAreSame,
} from "../../common/utils";
import { getDocDataAsArray, isDocContentSame, readContent } from "../../lib/src/common/utils";
import { shouldBeIgnored } from "../../lib/src/string_and_binary/path";
import type { ICoreModule } from "../ModuleTypes";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
export class ModuleFileHandler extends AbstractModule implements ICoreModule {
get db() {
return this.core.databaseFileAccess;
}
@@ -31,10 +44,14 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
if (!readFile) {
throw new Error(`File ${file.path} is not exist on the storage`);
}
return readFile
return readFile;
}
async storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force: boolean = false, onlyChunks: boolean = false): Promise<boolean | undefined> {
async storeFileToDB(
info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix,
force: boolean = false,
onlyChunks: boolean = false
): Promise<boolean | undefined> {
const file = typeof info === "string" ? this.storage.getFileStub(info) : info;
if (file == null) {
this._log(`File ${info} is not exist on the storage`, LOG_LEVEL_VERBOSE);
@@ -42,10 +59,13 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
}
// const file = item.args.file;
if (file.isInternal) {
this._log(`Internal file ${file.path} is not allowed to be processed on processFileEvent`, LOG_LEVEL_VERBOSE);
this._log(
`Internal file ${file.path} is not allowed to be processed on processFileEvent`,
LOG_LEVEL_VERBOSE
);
return false;
}
// First, check the file on the database
// First, check the file on the database
const entry = await this.db.fetchEntry(file, undefined, true, true);
if (!entry || entry.deleted || entry._deleted) {
@@ -54,21 +74,9 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
if (!onlyChunks) {
return await this.db.store(readFile);
} else {
return true;
return await this.db.createChunks(readFile, false, true);
}
}
// I remember that it should be processed naturally. -->
// // If the file is exist on the database, then it should be updated.
// // Check the file is already conflicted or not.
// const conflictedRevs = await this.db.getConflictedRevs(file);
// if (conflictedRevs.length > 0) {
// // If conflicted, then it should be stored as new conflicted file.
// const readFile = await this.readFileFromStub(file);
// this.db.store(readFile, true);
// return false;
// }
//< --
// entry is exist on the database, check the difference between the file and the entry.
@@ -126,10 +134,13 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
}
// const file = item.args.file;
if (file.isInternal) {
this._log(`Internal file ${file.path} is not allowed to be processed on processFileEvent`, LOG_LEVEL_VERBOSE);
this._log(
`Internal file ${file.path} is not allowed to be processed on processFileEvent`,
LOG_LEVEL_VERBOSE
);
return false;
}
// First, check the file on the database
// First, check the file on the database
const entry = await this.db.fetchEntry(file, undefined, true, true);
if (!entry || entry.deleted || entry._deleted) {
this._log(`File ${file.path} is not exist or already deleted on the database`, LOG_LEVEL_VERBOSE);
@@ -147,24 +158,39 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
return await this.db.delete(file);
}
async deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise<boolean | undefined> {
async deleteRevisionFromDB(
info: UXFileInfoStub | FilePath | FilePathWithPrefix,
rev: string
): Promise<boolean | undefined> {
//TODO: Possibly check the conflicting.
return await this.db.delete(info, rev);
}
async resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise<boolean | undefined> {
if (!await this.deleteRevisionFromDB(info, rev)) {
this._log(`Failed to delete the conflicted revision ${rev} of ${getPathFromUXFileInfo(info)}`, LOG_LEVEL_VERBOSE);
async resolveConflictedByDeletingRevision(
info: UXFileInfoStub | FilePath,
rev: string
): Promise<boolean | undefined> {
if (!(await this.deleteRevisionFromDB(info, rev))) {
this._log(
`Failed to delete the conflicted revision ${rev} of ${getPathFromUXFileInfo(info)}`,
LOG_LEVEL_VERBOSE
);
return false;
}
if (!await this.dbToStorageWithSpecificRev(info, rev, true)) {
this._log(`Failed to apply the resolved revision ${rev} of ${getPathFromUXFileInfo(info)} to the storage`, LOG_LEVEL_VERBOSE);
if (!(await this.dbToStorageWithSpecificRev(info, rev, true))) {
this._log(
`Failed to apply the resolved revision ${rev} of ${getPathFromUXFileInfo(info)} to the storage`,
LOG_LEVEL_VERBOSE
);
return false;
}
}
async dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | null, rev: string, force?: boolean): Promise<boolean> {
async dbToStorageWithSpecificRev(
info: UXFileInfoStub | UXFileInfo | FilePath | null,
rev: string,
force?: boolean
): Promise<boolean> {
const file = typeof info === "string" ? this.storage.getFileStub(info) : info;
if (file == null) {
this._log(`File ${info} is not exist on the storage`, LOG_LEVEL_VERBOSE);
@@ -178,12 +204,18 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
return await this.dbToStorage(docEntry, file, force);
}
async dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise<boolean> {
async dbToStorage(
entryInfo: MetaEntry | FilePathWithPrefix,
info: UXFileInfoStub | UXFileInfo | FilePath | null,
force?: boolean
): Promise<boolean> {
const file = typeof info === "string" ? this.storage.getFileStub(info) : info;
const mode = file == null ? "create" : "modify";
const docEntry = typeof entryInfo === "string" ?
await this.db.fetchEntryMeta(entryInfo, undefined, true) : await this.db.fetchEntryMeta(entryInfo.path, undefined, true);
const docEntry =
typeof entryInfo === "string"
? await this.db.fetchEntryMeta(entryInfo, undefined, true)
: await this.db.fetchEntryMeta(entryInfo.path, undefined, true);
if (!docEntry) {
this._log(`File ${entryInfo} is not exist on the database`, LOG_LEVEL_VERBOSE);
return false;
@@ -235,12 +267,9 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
}
const docData = readContent(docRead);
if (!existOnStorage) {
// The file is not exist on the storage. We do not care about the differences.
await this.storage.ensureDir(path);
return await this.storage.writeFileAuto(path, docData, { ctime: docRead.ctime, mtime: docRead.mtime });
}
if (!force) {
if (existOnStorage && !force) {
// The file is exist on the storage. Let's check the difference between the file and the entry.
// But, if force is true, then it should be updated.
// Ok, we have to compare.
let shouldApplied = false;
// 1. if the time stamp is far different, then it should be updated.
@@ -269,6 +298,11 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
return true;
}
// Let's apply the changes.
} else {
this._log(
`File ${docRead.path} ${existOnStorage ? "(new) " : ""} ${force ? " (forced)" : ""}`,
LOG_LEVEL_VERBOSE
);
}
await this.storage.ensureDir(path);
const ret = await this.storage.writeFileAuto(path, docData, { ctime: docRead.ctime, mtime: docRead.mtime });
@@ -281,7 +315,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
const eventItem = item.args;
const type = item.type;
const path = eventItem.file.path;
if (!await this.core.$$isTargetFile(path)) {
if (!(await this.core.$$isTargetFile(path))) {
this._log(`File ${path} is not the target file`, LOG_LEVEL_VERBOSE);
return false;
}
@@ -309,7 +343,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
async $anyProcessReplicatedDoc(entry: MetaEntry): Promise<boolean | undefined> {
return await serialized(entry.path, async () => {
if (!await this.core.$$isTargetFile(entry.path)) {
if (!(await this.core.$$isTargetFile(entry.path))) {
this._log(`File ${entry.path} is not the target file`, LOG_LEVEL_VERBOSE);
return false;
}
@@ -325,13 +359,15 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
// Nothing to do and other modules should also nothing to do.
return true;
} else {
this._log(`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`, LOG_LEVEL_VERBOSE);
this._log(
`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`,
LOG_LEVEL_VERBOSE
);
const ret = await this.dbToStorage(entry, targetFile);
this._log(`Processing ${path} (${entry._id.substring(0, 8)} :${entry._rev?.substring(0, 5)}) : Done`);
return ret;
}
});
}
async createAllChunks(showingNotice?: boolean): Promise<void> {
@@ -342,11 +378,16 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
const filesStorageSrc = this.storage.getFiles();
const incProcessed = () => {
processed++;
if (processed % 25 == 0) this._log(`Creating missing chunks: ${processed} of ${total} files`, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "chunkCreation");
}
if (processed % 25 == 0)
this._log(
`Creating missing chunks: ${processed} of ${total} files`,
showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO,
"chunkCreation"
);
};
const total = filesStorageSrc.length;
const procAllChunks = filesStorageSrc.map(async (file) => {
if (!await this.core.$$isTargetFile(file)) {
if (!(await this.core.$$isTargetFile(file))) {
incProcessed();
return true;
}
@@ -365,6 +406,10 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
}
});
await Promise.all(procAllChunks);
this._log(`Creating chunks Done: ${processed} of ${total} files`, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "chunkCreation");
this._log(
`Creating chunks Done: ${processed} of ${total} files`,
showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO,
"chunkCreation"
);
}
}
}

View File

@@ -5,7 +5,6 @@ import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleLocalDatabaseObsidian extends AbstractModule implements ICoreModule {
$everyOnloadStart(): Promise<boolean> {
return Promise.resolve(true);
}
@@ -20,4 +19,7 @@ export class ModuleLocalDatabaseObsidian extends AbstractModule implements ICore
return await this.localDatabase.initializeDatabase();
}
}
$$isDatabaseReady(): boolean {
return this.localDatabase != null && this.localDatabase.isReady;
}
}

View File

@@ -3,7 +3,6 @@ import { AbstractModule } from "../AbstractModule";
import type { ICoreModule } from "../ModuleTypes";
export class ModulePeriodicProcess extends AbstractModule implements ICoreModule {
periodicSyncProcessor = new PeriodicProcessor(this.core, async () => await this.core.$$replicate());
_disablePeriodic() {
@@ -11,20 +10,19 @@ export class ModulePeriodicProcess extends AbstractModule implements ICoreModule
return Promise.resolve(true);
}
_resumePeriodic() {
this.periodicSyncProcessor.enable(this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0);
this.periodicSyncProcessor.enable(
this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0
);
return Promise.resolve(true);
}
$allOnUnload() {
return this._disablePeriodic();
}
$everyBeforeRealizeSetting(): Promise<boolean> {
return this._disablePeriodic();
}
$everyBeforeSuspendProcess(): Promise<boolean> {
return this._disablePeriodic();
}
$everyAfterResumeProcess(): Promise<boolean> {
return this._resumePeriodic();
@@ -32,4 +30,4 @@ export class ModulePeriodicProcess extends AbstractModule implements ICoreModule
$everyAfterRealizeSetting(): Promise<boolean> {
return this._resumePeriodic();
}
}
}

View File

@@ -3,7 +3,10 @@ import type { ICoreModule } from "../ModuleTypes";
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
export class ModulePouchDB extends AbstractModule implements ICoreModule {
$$createPouchDBInstance<T extends object>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
$$createPouchDBInstance<T extends object>(
name?: string,
options?: PouchDB.Configuration.DatabaseConfiguration
): PouchDB.Database<T> {
const optionPass = options ?? {};
if (this.settings.useIndexedDBAdapter) {
optionPass.adapter = "indexeddb";
@@ -13,4 +16,4 @@ export class ModulePouchDB extends AbstractModule implements ICoreModule {
}
return new PouchDB(name, optionPass);
}
}
}

View File

@@ -1,5 +1,12 @@
import { delay } from "octagonal-wheels/promises";
import { FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, REMOTE_COUCHDB, REMOTE_MINIO } from "../../lib/src/common/types.ts";
import {
FLAGMD_REDFLAG2_HR,
FLAGMD_REDFLAG3_HR,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
REMOTE_COUCHDB,
REMOTE_MINIO,
} from "../../lib/src/common/types.ts";
import { AbstractModule } from "../AbstractModule.ts";
import type { Rebuilder } from "../interfaces/DatabaseRebuilder.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
@@ -7,12 +14,13 @@ import type { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchd
import { fetchAllUsedChunks } from "../../lib/src/pouchdb/utils_couchdb.ts";
export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebuilder {
$everyOnload(): Promise<boolean> {
this.core.rebuilder = this;
return Promise.resolve(true);
}
async $performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise<void> {
async $performRebuildDB(
method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"
): Promise<void> {
if (method == "localOnly") {
await this.$fetchLocal();
}
@@ -27,31 +35,41 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
}
}
async askUsingOptionalFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) {
if (
(await this.core.confirm.askYesNoDialog(
"Do you want to enable extra features? If you are new to Self-hosted LiveSync, try the core feature first!",
{ title: "Enable extra features", defaultOption: "No", timeout: 15 }
)) == "yes"
) {
await this.core.$allAskUsingOptionalSyncFeature(opt);
}
}
async rebuildRemote() {
await this.core.$allSuspendExtraSync();
this.core.settings.isConfigured = true;
await this.core.realizeSettingSyncMode();
await this.core.$$realizeSettingSyncMode();
await this.core.$$markRemoteLocked();
await this.core.$$tryResetRemoteDatabase();
await this.core.$$markRemoteLocked();
await delay(500);
await this.core.$allAskUsingOptionalSyncFeature({ enableOverwrite: true });
await this.askUsingOptionalFeature({ enableOverwrite: true });
await delay(1000);
await this.core.$$replicateAllToServer(true);
await delay(1000);
await this.core.$$replicateAllToServer(true);
await this.core.$$replicateAllToServer(true, true);
}
$rebuildRemote(): Promise<void> {
return this.rebuildRemote();
}
async rebuildEverything() {
await this.core.$allSuspendExtraSync();
await this.askUseNewAdapter();
this.core.settings.isConfigured = true;
await this.core.realizeSettingSyncMode();
await this.core.$$realizeSettingSyncMode();
await this.resetLocalDatabase();
await delay(1000);
await this.core.$$initializeDatabase(true);
@@ -59,12 +77,11 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
await this.core.$$tryResetRemoteDatabase();
await this.core.$$markRemoteLocked();
await delay(500);
await this.core.$allAskUsingOptionalSyncFeature({ enableOverwrite: true });
await this.askUsingOptionalFeature({ enableOverwrite: true });
await delay(1000);
await this.core.$$replicateAllToServer(true);
await delay(1000);
await this.core.$$replicateAllToServer(true);
await this.core.$$replicateAllToServer(true, true);
}
$rebuildEverything(): Promise<void> {
@@ -107,7 +124,6 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
await this.localDatabase.resetDatabase();
}
async suspendAllSync() {
this.core.settings.liveSync = false;
this.core.settings.periodicReplication = false;
@@ -121,7 +137,10 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
async suspendReflectingDatabase() {
if (this.core.settings.doNotSuspendOnFetching) return;
if (this.core.settings.remoteType == REMOTE_MINIO) return;
this._log(`Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`, LOG_LEVEL_NOTICE);
this._log(
`Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`,
LOG_LEVEL_NOTICE
);
this.core.settings.suspendParseReplicationResult = true;
this.core.settings.suspendFileWatching = true;
await this.core.saveSettings();
@@ -135,7 +154,6 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
await this.core.$$performFullScan(true);
await this.core.$everyBeforeReplicate(false); //TODO: Check actual need of this.
await this.core.saveSettings();
}
async askUseNewAdapter() {
if (!this.core.settings.useIndexedDBAdapter) {
@@ -144,7 +162,13 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
const CHOICE_NO = "No, keep compatibility";
const choices = [CHOICE_YES, CHOICE_NO];
const ret = await this.core.confirm.confirmWithMessage("Database adapter", message, choices, CHOICE_YES, 10);
const ret = await this.core.confirm.confirmWithMessage(
"Database adapter",
message,
choices,
CHOICE_YES,
10
);
if (ret == CHOICE_YES) {
this.core.settings.useIndexedDBAdapter = true;
}
@@ -155,11 +179,12 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
await this.askUseNewAdapter();
this.core.settings.isConfigured = true;
await this.suspendReflectingDatabase();
await this.core.realizeSettingSyncMode();
await this.core.$$realizeSettingSyncMode();
await this.resetLocalDatabase();
await delay(1000);
await this.core.$$openDatabase();
this.core.isReady = true;
// this.core.isReady = true;
this.core.$$markIsReady();
if (makeLocalChunkBeforeSync) {
await this.core.fileHandler.createAllChunks(true);
}
@@ -169,7 +194,7 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
await delay(1000);
await this.core.$$replicateAllFromServer(true);
await this.resumeReflectingDatabase();
await this.core.$allAskUsingOptionalSyncFeature({ enableFetch: true });
await this.askUsingOptionalFeature({ enableFetch: true });
}
async fetchLocalWithRebuild() {
return await this.fetchLocal(true);
@@ -190,10 +215,18 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
await this.core.$$resetLocalDatabase();
}
async fetchRemoteChunks() {
if (!this.core.settings.doNotSuspendOnFetching && this.core.settings.readChunksOnline && this.core.settings.remoteType == REMOTE_COUCHDB) {
if (
!this.core.settings.doNotSuspendOnFetching &&
this.core.settings.readChunksOnline &&
this.core.settings.remoteType == REMOTE_COUCHDB
) {
this._log(`Fetching chunks`, LOG_LEVEL_NOTICE);
const replicator = this.core.getReplicator() as LiveSyncCouchDBReplicator;
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(this.settings, this.core.getIsMobile(), true);
const replicator = this.core.$$getReplicator() as LiveSyncCouchDBReplicator;
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
this.settings,
this.core.$$isMobile(),
true
);
if (typeof remoteDB == "string") {
this._log(remoteDB, LOG_LEVEL_NOTICE);
} else {
@@ -208,9 +241,14 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
let i = 0;
for (const file of files) {
if (i++ % 10) this._log(`Check and Processing ${i} / ${files.length}`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
if (i++ % 10)
this._log(
`Check and Processing ${i} / ${files.length}`,
LOG_LEVEL_NOTICE,
"resolveAllConflictedFilesByNewerOnes"
);
await this.core.$anyResolveConflictByNewest(file);
}
this._log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
}
}
}

View File

@@ -8,21 +8,28 @@ import { purgeUnreferencedChunks, balanceChunkPurgedDBs } from "../../lib/src/po
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
import { throttle } from "octagonal-wheels/function";
import { arrayToChunkedArray } from "octagonal-wheels/collection";
import { SYNCINFO_ID, VER, type EntryBody, type EntryDoc, type LoadedEntry, type MetaEntry } from "../../lib/src/common/types";
import {
SYNCINFO_ID,
VER,
type EntryBody,
type EntryDoc,
type LoadedEntry,
type MetaEntry,
} from "../../lib/src/common/types";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { getPath, isChunk, isValidPath, scheduleTask } from "../../common/utils";
import { sendValue } from "octagonal-wheels/messagepassing/signal";
import { isAnyNote } from "../../lib/src/common/utils";
import { EVENT_FILE_SAVED, eventHub } from "../../common/events";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
export class ModuleReplicator extends AbstractModule implements ICoreModule {
$everyOnloadAfterLoadSettings(): Promise<boolean> {
eventHub.onEvent(EVENT_FILE_SAVED, () => {
if (this.settings.syncOnSave && !this.core.suspended) {
if (this.settings.syncOnSave && !this.core.$$isSuspended()) {
scheduleTask("perform-replicate-after-save", 250, () => this.core.$$waitForReplicationOnce());
}
})
});
return Promise.resolve(true);
}
@@ -30,19 +37,23 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
const replicator = await this.core.$anyNewReplicator();
if (!replicator) {
this._log("No replicator is available, this is the fatal error.", LOG_LEVEL_NOTICE);
return false
return false;
}
this.core.replicator = replicator;
await yieldMicrotask();
return true;
}
$$getReplicator(): LiveSyncAbstractReplicator {
return this.core.replicator;
}
$everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return this.setReplicator();
}
$everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return this.setReplicator();
}
async $everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
@@ -50,8 +61,8 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
return true;
}
async $$replicate(showMessage: boolean = false): Promise<boolean | void> {
//--?
if (!this.core.isReady) return;
//--?
if (!this.core.$$isReady()) return;
if (isLockAcquired("cleanup")) {
Logger("Database cleaning up is in process. replication has been cancelled", LOG_LEVEL_NOTICE);
return;
@@ -60,11 +71,11 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
Logger("Open settings and check message, please. replication has been cancelled.", LOG_LEVEL_NOTICE);
return;
}
if (!await this.core.$everyCommitPendingFileEvent()) {
if (!(await this.core.$everyCommitPendingFileEvent())) {
Logger("Some file events are pending. Replication has been cancelled.", LOG_LEVEL_NOTICE);
return false;
}
if (!await this.core.$everyBeforeReplicate(showMessage)) {
if (!(await this.core.$everyBeforeReplicate(showMessage))) {
Logger(`Replication has been cancelled by some module failure`, LOG_LEVEL_NOTICE);
return false;
}
@@ -74,32 +85,41 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
if (!ret) {
if (this.core.replicator.tweakSettingsMismatched) {
await this.core.$$askResolvingMismatchedTweaks();
} else {
if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) {
if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
Logger(
`The remote database has been cleaned.`,
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
await skipIfDuplicated("cleanup", async () => {
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
const message = `The remote database has been cleaned up.
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
However, If there are many chunks to be deleted, maybe fetching again is faster.
We will lose the history of this device if we fetch the remote database again.
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`;
const CHOICE_FETCH = "Fetch again";
const CHOICE_CLEAN = "Cleanup";
const CHOICE_DISMISS = "Dismiss";
const ret = await this.core.confirm.confirmWithMessage("Cleaned", message, [
CHOICE_FETCH,
CHOICE_CLEAN,
CHOICE_DISMISS], CHOICE_DISMISS, 30);
const ret = await this.core.confirm.confirmWithMessage(
"Cleaned",
message,
[CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS],
CHOICE_DISMISS,
30
);
if (ret == CHOICE_FETCH) {
await this.core.rebuilder.$performRebuildDB("localOnly");
}
if (ret == CHOICE_CLEAN) {
const replicator = this.core.getReplicator();
const replicator = this.core.$$getReplicator();
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(this.settings, this.core.getIsMobile(), true);
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
this.settings,
this.core.$$isMobile(),
true
);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL_NOTICE);
return false;
@@ -108,16 +128,23 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.hashCaches.clear();
// Perform the synchronisation once.
if (await this.core.replicator.openReplication(this.settings, false, showMessage, true)) {
if (
await this.core.replicator.openReplication(this.settings, false, showMessage, true)
) {
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.hashCaches.clear();
await this.core.getReplicator().markRemoteResolved(this.settings);
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO)
await this.core.$$getReplicator().markRemoteResolved(this.settings);
Logger(
"The local database has been cleaned up.",
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
} else {
Logger("Replication has been cancelled. Please try it again.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO)
Logger(
"Replication has been cancelled. Please try it again.",
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
}
}
});
} else {
@@ -125,23 +152,30 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
The remote database has been rebuilt.
To synchronize, this device must fetch everything again once.
Or if you are sure know what had been happened, we can unlock the database from the setting dialog.
`
`;
const CHOICE_FETCH = "Fetch again";
const CHOICE_DISMISS = "Dismiss";
const ret = await this.core.confirm.confirmWithMessage("Locked", message, [
CHOICE_FETCH,
CHOICE_DISMISS], CHOICE_DISMISS, 10);
const ret = await this.core.confirm.confirmWithMessage(
"Locked",
message,
[CHOICE_FETCH, CHOICE_DISMISS],
CHOICE_DISMISS,
10
);
if (ret == CHOICE_FETCH) {
const CHOICE_RESTART = "Restart";
const CHOICE_WITHOUT_RESTART = "Without restart";
if (await this.core.confirm.askSelectStringDialogue(
"Self-hosted LiveSync restarts Obsidian to fetch everything safely. However, you can do it without restarting. Please choose one.",
[CHOICE_RESTART, CHOICE_WITHOUT_RESTART], {
title: "Fetch again",
defaultAction: CHOICE_RESTART,
timeout: 30,
}
) == CHOICE_RESTART) {
if (
(await this.core.confirm.askSelectStringDialogue(
"Self-hosted LiveSync restarts Obsidian to fetch everything safely. However, you can do it without restarting. Please choose one.",
[CHOICE_RESTART, CHOICE_WITHOUT_RESTART],
{
title: "Fetch again",
defaultAction: CHOICE_RESTART,
timeout: 30,
}
)) == CHOICE_RESTART
) {
await this.core.rebuilder.scheduleFetch();
// await this.core.$$scheduleAppReload();
return;
@@ -159,16 +193,18 @@ Or if you are sure know what had been happened, we can unlock the database from
$$parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
if (this.settings.suspendParseReplicationResult && !this.replicationResultProcessor.isSuspended) {
this.replicationResultProcessor.suspend()
this.replicationResultProcessor.suspend();
}
this.replicationResultProcessor.enqueueAll(docs);
if (!this.settings.suspendParseReplicationResult && this.replicationResultProcessor.isSuspended) {
this.replicationResultProcessor.resume()
this.replicationResultProcessor.resume();
}
}
_saveQueuedFiles = throttle(() => {
const saveData = this.replicationResultProcessor._queue.filter(e => e !== undefined && e !== null).map((e) => e?._id ?? "" as string) as string[];
const kvDBKey = "queued-files"
const saveData = this.replicationResultProcessor._queue
.filter((e) => e !== undefined && e !== null)
.map((e) => e?._id ?? ("" as string)) as string[];
const kvDBKey = "queued-files";
// localStorage.setItem(lsKey, saveData);
fireAndForget(() => this.core.kvDB.set(kvDBKey, saveData));
}, 100);
@@ -178,19 +214,19 @@ Or if you are sure know what had been happened, we can unlock the database from
async loadQueuedFiles() {
if (this.settings.suspendParseReplicationResult) return;
if (!this.settings.isConfigured) return;
const kvDBKey = "queued-files"
const kvDBKey = "queued-files";
// const ids = [...new Set(JSON.parse(localStorage.getItem(lsKey) || "[]"))] as string[];
const ids = [...new Set(await this.core.kvDB.get<string[]>(kvDBKey) ?? [])];
const ids = [...new Set((await this.core.kvDB.get<string[]>(kvDBKey)) ?? [])];
const batchSize = 100;
const chunkedIds = arrayToChunkedArray(ids, batchSize);
for await (const idsBatch of chunkedIds) {
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({
keys: idsBatch,
include_docs: true,
limit: 100
limit: 100,
});
const docs = ret.rows.filter(e => e.doc).map(e => e.doc) as PouchDB.Core.ExistingDocument<EntryDoc>[];
const errors = ret.rows.filter(e => !e.doc && !e.value.deleted);
const docs = ret.rows.filter((e) => e.doc).map((e) => e.doc) as PouchDB.Core.ExistingDocument<EntryDoc>[];
const errors = ret.rows.filter((e) => !e.doc && !e.value.deleted);
if (errors.length > 0) {
Logger("Some queued processes were not resurrected");
Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE);
@@ -198,125 +234,165 @@ Or if you are sure know what had been happened, we can unlock the database from
this.replicationResultProcessor.enqueueAll(docs);
await this.replicationResultProcessor.waitForAllProcessed();
}
}
replicationResultProcessor = new QueueProcessor(async (docs: PouchDB.Core.ExistingDocument<EntryDoc>[]) => {
if (this.settings.suspendParseReplicationResult) return;
const change = docs[0];
if (!change) return;
if (isChunk(change._id)) {
// SendSignal?
// this.parseIncomingChunk(change);
sendValue(`leaf-${change._id}`, change);
return;
}
if (await this.core.$anyModuleParsedReplicationResultItem(change)) return;
// any addon needs this item?
// for (const proc of this.core.addOns) {
// if (await proc.parseReplicationResultItem(change)) {
// return;
// }
// }
if (change.type == "versioninfo") {
if (change.version > VER) {
this.core.replicator.closeReplication();
Logger(`Remote database updated to incompatible version. update your Self-hosted LiveSync plugin.`, LOG_LEVEL_NOTICE);
}
return;
}
if (change._id == SYNCINFO_ID || // Synchronisation information data
change._id.startsWith("_design") //design document
) {
return;
}
if (isAnyNote(change)) {
if (this.databaseQueuedProcessor._isSuspended) {
Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL_INFO);
}
const size = change.size;
if (this.core.$$isFileSizeExceeded(size)) {
Logger(`Processing ${change.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
replicationResultProcessor = new QueueProcessor(
async (docs: PouchDB.Core.ExistingDocument<EntryDoc>[]) => {
if (this.settings.suspendParseReplicationResult) return;
const change = docs[0];
if (!change) return;
if (isChunk(change._id)) {
// SendSignal?
// this.parseIncomingChunk(change);
sendValue(`leaf-${change._id}`, change);
return;
}
this.databaseQueuedProcessor.enqueue(change);
}
return;
}, {
batchSize: 1,
suspended: true,
concurrentLimit: 100,
delay: 0,
totalRemainingReactiveSource: this.core.replicationResultCount
}).replaceEnqueueProcessor((queue, newItem) => {
const q = queue.filter(e => e._id != newItem._id);
return [...q, newItem];
}).startPipeline().onUpdateProgress(() => {
this.saveQueuedFiles();
});
databaseQueuedProcessor = new QueueProcessor(async (docs: EntryBody[]) => {
const dbDoc = docs[0] as LoadedEntry; // It has no `data`
const path = getPath(dbDoc);
// If `Read chunks online` is disabled, chunks should be transferred before here.
// However, in some cases, chunks are after that. So, if missing chunks exist, we have to wait for them.
const doc = await this.localDatabase.getDBEntryFromMeta({ ...dbDoc }, {}, false, true, true);
if (!doc) {
Logger(`Something went wrong while gathering content of ${path} (${dbDoc._id.substring(0, 8)}, ${dbDoc._rev?.substring(0, 10)}) `, LOG_LEVEL_NOTICE)
if (await this.core.$anyModuleParsedReplicationResultItem(change)) return;
// any addon needs this item?
// for (const proc of this.core.addOns) {
// if (await proc.parseReplicationResultItem(change)) {
// return;
// }
// }
if (change.type == "versioninfo") {
if (change.version > VER) {
this.core.replicator.closeReplication();
Logger(
`Remote database updated to incompatible version. update your Self-hosted LiveSync plugin.`,
LOG_LEVEL_NOTICE
);
}
return;
}
if (
change._id == SYNCINFO_ID || // Synchronisation information data
change._id.startsWith("_design") //design document
) {
return;
}
if (isAnyNote(change)) {
const docPath = getPath(change);
if (!(await this.core.$$isTargetFile(docPath))) {
Logger(`Skipped: ${docPath}`, LOG_LEVEL_VERBOSE);
return;
}
if (this.databaseQueuedProcessor._isSuspended) {
Logger(`Processing scheduled: ${docPath}`, LOG_LEVEL_INFO);
}
const size = change.size;
if (this.core.$$isFileSizeExceeded(size)) {
Logger(
`Processing ${docPath} has been skipped due to file size exceeding the limit`,
LOG_LEVEL_NOTICE
);
return;
}
this.databaseQueuedProcessor.enqueue(change);
}
return;
},
{
batchSize: 1,
suspended: true,
concurrentLimit: 100,
delay: 0,
totalRemainingReactiveSource: this.core.replicationResultCount,
}
)
.replaceEnqueueProcessor((queue, newItem) => {
const q = queue.filter((e) => e._id != newItem._id);
return [...q, newItem];
})
.startPipeline()
.onUpdateProgress(() => {
this.saveQueuedFiles();
});
if (await this.core.$anyProcessOptionalSyncFiles(dbDoc)) {
// Already processed
} else if (isValidPath(getPath(doc))) {
this.storageApplyingProcessor.enqueue(doc as MetaEntry);
} else {
Logger(`Skipped: ${doc._id.substring(0, 8)}`, LOG_LEVEL_VERBOSE);
databaseQueuedProcessor = new QueueProcessor(
async (docs: EntryBody[]) => {
const dbDoc = docs[0] as LoadedEntry; // It has no `data`
const path = getPath(dbDoc);
// If `Read chunks online` is disabled, chunks should be transferred before here.
// However, in some cases, chunks are after that. So, if missing chunks exist, we have to wait for them.
const doc = await this.localDatabase.getDBEntryFromMeta({ ...dbDoc }, {}, false, true, true);
if (!doc) {
Logger(
`Something went wrong while gathering content of ${path} (${dbDoc._id.substring(0, 8)}, ${dbDoc._rev?.substring(0, 10)}) `,
LOG_LEVEL_NOTICE
);
return;
}
if (await this.core.$anyProcessOptionalSyncFiles(dbDoc)) {
// Already processed
} else if (isValidPath(getPath(doc))) {
this.storageApplyingProcessor.enqueue(doc as MetaEntry);
} else {
Logger(`Skipped: ${doc._id.substring(0, 8)}`, LOG_LEVEL_VERBOSE);
}
return;
},
{
suspended: true,
batchSize: 1,
concurrentLimit: 10,
yieldThreshold: 1,
delay: 0,
totalRemainingReactiveSource: this.core.databaseQueueCount,
}
return;
}, {
suspended: true,
batchSize: 1,
concurrentLimit: 10,
yieldThreshold: 1,
delay: 0,
totalRemainingReactiveSource: this.core.databaseQueueCount
}).replaceEnqueueProcessor((queue, newItem) => {
const q = queue.filter(e => e._id != newItem._id);
return [...q, newItem];
}).startPipeline();
)
.replaceEnqueueProcessor((queue, newItem) => {
const q = queue.filter((e) => e._id != newItem._id);
return [...q, newItem];
})
.startPipeline();
storageApplyingProcessor = new QueueProcessor(async (docs: MetaEntry[]) => {
const entry = docs[0];
await this.core.$anyProcessReplicatedDoc(entry);
return;
}, {
suspended: true,
batchSize: 1,
concurrentLimit: 6,
yieldThreshold: 1,
delay: 0,
totalRemainingReactiveSource: this.core.storageApplyingCount
}).replaceEnqueueProcessor((queue, newItem) => {
const q = queue.filter(e => e._id != newItem._id);
return [...q, newItem];
}).startPipeline()
storageApplyingProcessor = new QueueProcessor(
async (docs: MetaEntry[]) => {
const entry = docs[0];
await this.core.$anyProcessReplicatedDoc(entry);
return;
},
{
suspended: true,
batchSize: 1,
concurrentLimit: 6,
yieldThreshold: 1,
delay: 0,
totalRemainingReactiveSource: this.core.storageApplyingCount,
}
)
.replaceEnqueueProcessor((queue, newItem) => {
const q = queue.filter((e) => e._id != newItem._id);
return [...q, newItem];
})
.startPipeline();
$everyBeforeSuspendProcess(): Promise<boolean> {
this.core.replicator.closeReplication();
return Promise.resolve(true);
}
async $$replicateAllToServer(showingNotice: boolean = false): Promise<boolean> {
if (!this.core.isReady) return false;
if (!await this.core.$everyBeforeReplicate(showingNotice)) {
async $$replicateAllToServer(
showingNotice: boolean = false,
sendChunksInBulkDisabled: boolean = false
): Promise<boolean> {
if (!this.core.$$isReady()) return false;
if (!(await this.core.$everyBeforeReplicate(showingNotice))) {
Logger(`Replication has been cancelled by some module failure`, LOG_LEVEL_NOTICE);
return false;
}
if (this.core.replicator instanceof LiveSyncCouchDBReplicator) {
if (await this.core.confirm.askYesNoDialog("Do you want to send all chunks before replication?", { defaultOption: "No", timeout: 20 }) == "yes") {
await this.core.replicator.sendChunks(this.core.settings, undefined, true, 0);
if (!sendChunksInBulkDisabled) {
if (this.core.replicator instanceof LiveSyncCouchDBReplicator) {
if (
(await this.core.confirm.askYesNoDialog("Do you want to send all chunks before replication?", {
defaultOption: "No",
timeout: 20,
})) == "yes"
) {
await this.core.replicator.sendChunks(this.core.settings, undefined, true, 0);
}
}
}
const ret = await this.core.replicator.replicateAllToServer(this.settings, showingNotice);
@@ -326,7 +402,7 @@ Or if you are sure know what had been happened, we can unlock the database from
return !checkResult;
}
async $$replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
if (!this.core.isReady) return false;
if (!this.core.$$isReady()) return false;
const ret = await this.core.replicator.replicateAllFromServer(this.settings, showingNotice);
if (ret) return true;
const checkResult = await this.core.$anyAfterConnectCheckFailed();
@@ -337,7 +413,4 @@ Or if you are sure know what had been happened, we can unlock the database from
async $$waitForReplicationOnce(): Promise<boolean | void> {
return await shareRunningResult(`replication`, () => this.core.$$replicate());
}
}
}

View File

@@ -10,10 +10,9 @@ export class ModuleReplicatorCouchDB extends AbstractModule implements ICoreModu
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 (settings.remoteType == REMOTE_MINIO) {
return undefined!
return undefined!;
}
return Promise.resolve(new LiveSyncCouchDBReplicator(this.core));
}
$everyAfterResumeProcess(): Promise<boolean> {
if (this.settings.remoteType != REMOTE_MINIO) {
@@ -30,4 +29,4 @@ export class ModuleReplicatorCouchDB extends AbstractModule implements ICoreModu
return Promise.resolve(true);
}
}
}

View File

@@ -12,6 +12,4 @@ export class ModuleReplicatorMinIO extends AbstractModule implements ICoreModule
}
return undefined!;
}
}
}

View File

@@ -1,22 +1,35 @@
import { LRUCache } from "octagonal-wheels/memory/LRUCache";
import { getPathFromUXFileInfo, id2path, isInternalMetadata, path2id, stripInternalMetadataPrefix, useMemo } from "../../common/utils";
import { LOG_LEVEL_VERBOSE, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix, type ObsidianLiveSyncSettings, type UXFileInfoStub } from "../../lib/src/common/types";
import {
getPathFromUXFileInfo,
id2path,
isInternalMetadata,
path2id,
stripInternalMetadataPrefix,
useMemo,
} from "../../common/utils";
import {
LOG_LEVEL_VERBOSE,
type DocumentID,
type EntryHasPath,
type FilePath,
type FilePathWithPrefix,
type ObsidianLiveSyncSettings,
type UXFileInfoStub,
} from "../../lib/src/common/types";
import { addPrefix, isAcceptedAll, stripAllPrefixes } from "../../lib/src/string_and_binary/path";
import { AbstractModule } from "../AbstractModule";
import type { ICoreModule } from "../ModuleTypes";
import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
import { isDirty } from "../../lib/src/common/utils";
export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
reloadIgnoreFiles() {
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
this.ignoreFiles = this.settings.ignoreFiles.split(",").map((e) => e.trim());
}
$everyOnload(): Promise<boolean> {
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: ObsidianLiveSyncSettings) => {
this.reloadIgnoreFiles();
});
eventHub.onEvent(EVENT_REQUEST_RELOAD_SETTING_TAB, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
eventHub.onEvent(EVENT_REQUEST_RELOAD_SETTING_TAB, () => {
this.reloadIgnoreFiles();
});
return Promise.resolve(true);
@@ -32,10 +45,13 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
}
async $$path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
const destPath = addPrefix(filename, prefix ?? "");
return await path2id(destPath, this.settings.usePathObfuscation ? this.settings.passphrase : "", !this.settings.handleFilenameCaseSensitive);
return await path2id(
destPath,
this.settings.usePathObfuscation ? this.settings.passphrase : "",
!this.settings.handleFilenameCaseSensitive
);
}
$$isFileSizeExceeded(size: number) {
if (this.settings.syncMaxSizeInMB > 0 && size > 0) {
if (this.settings.syncMaxSizeInMB * 1024 * 1024 < size) {
@@ -45,57 +61,62 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
return false;
}
$$markFileListPossiblyChanged(): void {
this.totalFileEventCount++;
}
totalFileEventCount = 0;
get fileListPossiblyChanged() {
if (isDirty("totalFileEventCount", this.core.totalFileEventCount)) {
if (isDirty("totalFileEventCount", this.totalFileEventCount)) {
return true;
}
return false;
}
async $$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false) {
const fileCount = useMemo<Record<string, number>>({
key: "fileCount", // forceUpdate: !keepFileCheckList,
}, (ctx, prev) => {
if (keepFileCheckList && prev) return prev;
if (!keepFileCheckList && prev && !this.fileListPossiblyChanged) {
return prev;
}
const fileList = (ctx.get("fileList") ?? []) as FilePathWithPrefix[];
// const fileNameList = (ctx.get("fileNameList") ?? []) as FilePath[];
// const fileNames =
const vaultFiles = this.core.storageAccess.getFileNames().sort();
if (prev && vaultFiles.length == fileList.length) {
const fl3 = new Set([...fileList, ...vaultFiles]);
if (fileList.length == fl3.size && vaultFiles.length == fl3.size) {
const fileCount = useMemo<Record<string, number>>(
{
key: "fileCount", // forceUpdate: !keepFileCheckList,
},
(ctx, prev) => {
if (keepFileCheckList && prev) return prev;
if (!keepFileCheckList && prev && !this.fileListPossiblyChanged) {
return prev;
}
}
ctx.set("fileList", vaultFiles);
const fileCount: Record<string, number> = {};
for (const file of vaultFiles) {
const lc = file.toLowerCase();
if (!fileCount[lc]) {
fileCount[lc] = 1;
} else {
fileCount[lc]++;
const fileList = (ctx.get("fileList") ?? []) as FilePathWithPrefix[];
// const fileNameList = (ctx.get("fileNameList") ?? []) as FilePath[];
// const fileNames =
const vaultFiles = this.core.storageAccess.getFileNames().sort();
if (prev && vaultFiles.length == fileList.length) {
const fl3 = new Set([...fileList, ...vaultFiles]);
if (fileList.length == fl3.size && vaultFiles.length == fl3.size) {
return prev;
}
}
ctx.set("fileList", vaultFiles);
const fileCount: Record<string, number> = {};
for (const file of vaultFiles) {
const lc = file.toLowerCase();
if (!fileCount[lc]) {
fileCount[lc] = 1;
} else {
fileCount[lc]++;
}
}
return fileCount;
}
return fileCount;
})
);
const filepath = getPathFromUXFileInfo(file);
const lc = filepath.toLowerCase();
if (this.core.shouldCheckCaseInsensitive) {
if (this.core.$$shouldCheckCaseInsensitive()) {
if (lc in fileCount && fileCount[lc] > 1) {
return false;
}
}
const fileNameLC = getPathFromUXFileInfo(file).split("/").pop()?.toLowerCase();
if (this.settings.useIgnoreFiles) {
if (this.ignoreFiles.some(e => e.toLowerCase() == fileNameLC)) {
if (this.ignoreFiles.some((e) => e.toLowerCase() == fileNameLC)) {
// We must reload ignore files due to the its change.
await this.readIgnoreFile(filepath);
}
@@ -104,11 +125,11 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
}
}
if (!this.localDatabase?.isTargetFile(filepath)) return false;
return true
return true;
}
ignoreFileCache = new LRUCache<string, string[] | false>(300, 250000, true);
ignoreFiles = [] as string[]
ignoreFiles = [] as string[];
async readIgnoreFile(path: string) {
try {
const file = await this.core.storageAccess.readFileText(path);
@@ -133,14 +154,18 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule {
if (!this.settings.useIgnoreFiles) {
return false;
}
const filepath = getPathFromUXFileInfo(file)
const filepath = getPathFromUXFileInfo(file);
if (this.ignoreFileCache.has(filepath)) {
// Renew
await this.readIgnoreFile(filepath);
}
if (!await isAcceptedAll(stripAllPrefixes(filepath), this.ignoreFiles, (filename) => this.getIgnoreFile(filename))) {
if (
!(await isAcceptedAll(stripAllPrefixes(filepath), this.ignoreFiles, (filename) =>
this.getIgnoreFile(filename)
))
) {
return true;
}
return false;
}
}
}

View File

@@ -1,39 +1,45 @@
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { AbstractModule } from "../AbstractModule.ts";
import { sizeToHumanReadable } from "octagonal-wheels/number";
import { delay } from "octagonal-wheels/promises";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleCheckRemoteSize extends AbstractModule implements ICoreModule {
async $allScanStat(): Promise<boolean> {
this._log(`Checking storage sizes`, LOG_LEVEL_VERBOSE);
if (this.settings.notifyThresholdOfRemoteStorageSize < 0) {
const message = `Now, Self-hosted LiveSync is able to check the remote storage size on the start-up.
const message = `We can set a maximum database capacity warning, **to take action before running out of space on the remote storage**.
Do you want to enable this?
You can configure the threshold size for your remote storage. This will be different for your server.
> [!MORE]-
> - 0: Do not warn about storage size.
> This is recommended if you have enough space on the remote storage especially you have self-hosted. And you can check the storage size and rebuild manually.
> - 800: Warn if the remote storage size exceeds 800MB.
> This is recommended if you are using fly.io with 1GB limit or IBM Cloudant.
> - 2000: Warn if the remote storage size exceeds 2GB.
Please choose the threshold size as you like.
If we have reached the limit, we will be asked to enlarge the limit step by step.
`;
const ANSWER_0 = "No, never warn please";
const ANSWER_800 = "800MB (Cloudant, fly.io)";
const ANSWER_2000 = "2GB (Standard)";
const ASK_ME_NEXT_TIME = "Ask me later";
- 0: Do not warn about storage size.
This is recommended if you have enough space on the remote storage especially you have self-hosted. And you can check the storage size and rebuild manually.
- 800: Warn if the remote storage size exceeds 800MB.
This is recommended if you are using fly.io with 1GB limit or IBM Cloudant.
- 2000: Warn if the remote storage size exceeds 2GB.
And if your actual storage size exceeds the threshold after the setup, you may warned again. But do not worry, you can enlarge the threshold (or rebuild everything to reduce the size).
`
const ANSWER_0 = "Do not warn";
const ANSWER_800 = "800MB";
const ANSWER_2000 = "2GB";
const ret = await this.core.confirm.confirmWithMessage("Remote storage size threshold", message, [ANSWER_0, ANSWER_800, ANSWER_2000], ANSWER_800, 40);
const ret = await this.core.confirm.askSelectStringDialogue(
message,
[ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME],
{
defaultAction: ASK_ME_NEXT_TIME,
title: "Setting up database size notification",
timeout: 40,
}
);
if (ret == ANSWER_0) {
this.settings.notifyThresholdOfRemoteStorageSize = 0;
await this.core.saveSettings();
} else if (ret == ANSWER_800) {
this.settings.notifyThresholdOfRemoteStorageSize = 800;
await this.core.saveSettings();
} else {
} else if (ret == ANSWER_2000) {
this.settings.notifyThresholdOfRemoteStorageSize = 2000;
await this.core.saveSettings();
}
@@ -45,39 +51,62 @@ And if your actual storage size exceeds the threshold after the setup, you may w
if (estimatedSize) {
const maxSize = this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024;
if (estimatedSize > maxSize) {
const message = `Remote storage size: ${sizeToHumanReadable(estimatedSize)}. It exceeds the configured value ${sizeToHumanReadable(maxSize)}.
This may cause the storage to be full. You should enlarge the remote storage, or rebuild everything to reduce the size. \n
**Note:** If you are new to Self-hosted LiveSync, you should enlarge the threshold. \n
const message = `**Your database is getting larger!** But do not worry, we can address it now. The time before running out of space on the remote storage.
Self-hosted LiveSync will not release the storage automatically even if the file is deleted. This is why they need regular maintenance.\n
| Measured size | Configured size |
| --- | --- |
| ${sizeToHumanReadable(estimatedSize)} | ${sizeToHumanReadable(maxSize)} |
If you have enough space on the remote storage, you can enlarge the threshold. Otherwise, you should rebuild everything.\n
> [!MORE]-
> If you have been using it for many years, there may be unreferenced chunks - that is, garbage - accumulating in the database. Therefore, we recommend rebuilding everything. It will probably become much smaller.
>
> If the volume of your vault is simply increasing, it is better to rebuild everything after organizing the files. Self-hosted LiveSync does not delete the actual data even if you delete it to speed up the process. It is roughly [documented](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).
>
> If you don't mind the increase, you can increase the notification limit by 100MB. This is the case if you are running it on your own server. However, it is better to rebuild everything from time to time.
>
> [!WARNING]
> If you perform rebuild everything, make sure all devices are synchronised. The plug-in will merge as much as possible, though.
However, **Please make sure that all devices have been synchronised**. \n
\n`;
const newMax = ~~(estimatedSize / 1024 / 1024) + 100;
const ANSWER_ENLARGE_LIMIT = `Enlarge to ${newMax}MB`;
const ANSWER_REBUILD = "Rebuild now";
const ANSWER_ENLARGE_LIMIT = `increase to ${newMax}MB`;
const ANSWER_REBUILD = "Rebuild Everything Now";
const ANSWER_IGNORE = "Dismiss";
const ret = await this.core.confirm.confirmWithMessage("Remote storage size exceeded", message, [ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE,], ANSWER_IGNORE, 20);
const ret = await this.core.confirm.askSelectStringDialogue(
message,
[ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE],
{
defaultAction: ANSWER_IGNORE,
title: "Remote storage size exceeded the limit",
timeout: 60,
}
);
if (ret == ANSWER_REBUILD) {
const ret = await this.core.confirm.askYesNoDialog("This may take a bit of a long time. Do you really want to rebuild everything now?", { defaultOption: "No" });
const ret = await this.core.confirm.askYesNoDialog(
"This may take a bit of a long time. Do you really want to rebuild everything now?",
{ defaultOption: "No" }
);
if (ret == "yes") {
this._log(`Receiving all from the server before rebuilding`, LOG_LEVEL_NOTICE);
await this.core.$$replicateAllFromServer(true);
await delay(3000);
this._log(`Obsidian will be reloaded to rebuild everything.`, LOG_LEVEL_NOTICE);
this.core.settings.notifyThresholdOfRemoteStorageSize = -1;
await this.saveSettings();
await this.core.rebuilder.scheduleRebuild();
}
} else if (ret == ANSWER_ENLARGE_LIMIT) {
this.settings.notifyThresholdOfRemoteStorageSize = ~~(estimatedSize / 1024 / 1024) + 100;
this._log(`Threshold has been enlarged to ${this.settings.notifyThresholdOfRemoteStorageSize}MB`, LOG_LEVEL_NOTICE);
this._log(
`Threshold has been enlarged to ${this.settings.notifyThresholdOfRemoteStorageSize}MB`,
LOG_LEVEL_NOTICE
);
await this.core.saveSettings();
} else {
// Dismiss or Close the dialog
}
this._log(`Remote storage size: ${sizeToHumanReadable(estimatedSize)} exceeded ${sizeToHumanReadable(this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024)} `, LOG_LEVEL_INFO);
this._log(
`Remote storage size: ${sizeToHumanReadable(estimatedSize)} exceeded ${sizeToHumanReadable(this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024)} `,
LOG_LEVEL_INFO
);
} else {
this._log(`Remote storage size: ${sizeToHumanReadable(estimatedSize)}`, LOG_LEVEL_INFO);
}
@@ -86,5 +115,4 @@ However, **Please make sure that all devices have been synchronised**. \n
}
return true;
}
}
}

View File

@@ -5,7 +5,6 @@ import { sendValue } from "octagonal-wheels/messagepassing/signal";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleConflictChecker extends AbstractModule implements ICoreModule {
async $$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
const path = file;
if (this.settings.checkConflictOnlyOnOpen) {
@@ -36,40 +35,44 @@ export class ModuleConflictChecker extends AbstractModule implements ICoreModule
}
// TODO-> Move to ModuleConflictResolver?
conflictResolveQueue = new QueueProcessor(async (filenames: FilePathWithPrefix[]) => {
await this.core.$$resolveConflict(filenames[0]);
}, {
suspended: false,
batchSize: 1,
concurrentLimit: 1,
delay: 10,
keepResultUntilDownstreamConnected: false
}).replaceEnqueueProcessor((queue, newEntity) => {
conflictResolveQueue = new QueueProcessor(
async (filenames: FilePathWithPrefix[]) => {
await this.core.$$resolveConflict(filenames[0]);
},
{
suspended: false,
batchSize: 1,
concurrentLimit: 1,
delay: 10,
keepResultUntilDownstreamConnected: false,
}
).replaceEnqueueProcessor((queue, newEntity) => {
const filename = newEntity;
sendValue("cancel-resolve-conflict:" + filename, true);
const newQueue = [...queue].filter(e => e != newEntity);
const newQueue = [...queue].filter((e) => e != newEntity);
return [...newQueue, newEntity];
});
conflictCheckQueue = // First process - Check is the file actually need resolve -
new QueueProcessor((files: FilePathWithPrefix[]) => {
const filename = files[0];
// const file = await this.core.storageAccess.isExists(filename);
// if (!file) return [];
// if (!(file instanceof TFile)) return;
// if ((file instanceof TFolder)) return [];
// Check again?
return Promise.resolve([filename]);
// this.conflictResolveQueue.enqueueWithKey(filename, { filename, file });
}, {
suspended: false,
batchSize: 1,
concurrentLimit: 5,
delay: 10,
keepResultUntilDownstreamConnected: true,
pipeTo: this.conflictResolveQueue,
totalRemainingReactiveSource: this.core.conflictProcessQueueCount
});
}
new QueueProcessor(
(files: FilePathWithPrefix[]) => {
const filename = files[0];
// const file = await this.core.storageAccess.isExists(filename);
// if (!file) return [];
// if (!(file instanceof TFile)) return;
// if ((file instanceof TFolder)) return [];
// Check again?
return Promise.resolve([filename]);
// this.conflictResolveQueue.enqueueWithKey(filename, { filename, file });
},
{
suspended: false,
batchSize: 1,
concurrentLimit: 5,
delay: 10,
keepResultUntilDownstreamConnected: true,
pipeTo: this.conflictResolveQueue,
totalRemainingReactiveSource: this.core.conflictProcessQueueCount,
}
);
}

View File

@@ -1,17 +1,32 @@
import { serialized } from "octagonal-wheels/concurrency/lock";
import { AbstractModule } from "../AbstractModule.ts";
import { AUTO_MERGED, CANCELLED, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, MISSING_OR_ERROR, NOT_CONFLICTED, type diff_check_result, type FilePathWithPrefix } from "../../lib/src/common/types";
import {
AUTO_MERGED,
CANCELLED,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
MISSING_OR_ERROR,
NOT_CONFLICTED,
type diff_check_result,
type FilePathWithPrefix,
} from "../../lib/src/common/types";
import { compareMTime, displayRev, TARGET_IS_NEW } from "../../common/utils";
import diff_match_patch from "diff-match-patch";
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleConflictResolver extends AbstractModule implements ICoreModule {
async $$resolveConflictByDeletingRev(path: FilePathWithPrefix, deleteRevision: string, subTitle = ""): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> {
async $$resolveConflictByDeletingRev(
path: FilePathWithPrefix,
deleteRevision: string,
subTitle = ""
): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> {
const title = `Resolving ${subTitle ? `[${subTitle}]` : ""}:`;
if (!await this.core.fileHandler.deleteRevisionFromDB(path, deleteRevision)) {
this._log(`${title} Could not delete conflicted revision ${displayRev(deleteRevision)} of ${path}`, LOG_LEVEL_NOTICE);
if (!(await this.core.fileHandler.deleteRevisionFromDB(path, deleteRevision))) {
this._log(
`${title} Could not delete conflicted revision ${displayRev(deleteRevision)} of ${path}`,
LOG_LEVEL_NOTICE
);
return MISSING_OR_ERROR;
}
this._log(`${title} Conflicted revision deleted ${displayRev(deleteRevision)} ${path}`, LOG_LEVEL_INFO);
@@ -20,7 +35,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
return AUTO_MERGED;
}
// If no conflicts were found, write the resolved content to the storage.
if (!await this.core.fileHandler.dbToStorage(path, stripAllPrefixes(path), true)) {
if (!(await this.core.fileHandler.dbToStorage(path, stripAllPrefixes(path), true))) {
this._log(`Could not write the resolved content to the storage: ${path}`, LOG_LEVEL_NOTICE);
return MISSING_OR_ERROR;
}
@@ -28,7 +43,6 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
return AUTO_MERGED;
}
async checkConflictAndPerformAutoMerge(path: FilePathWithPrefix): Promise<diff_check_result> {
//
const ret = await this.localDatabase.tryAutoMerge(path, !this.settings.disableMarkdownAutoMerge);
@@ -40,7 +54,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
const p = ret.result;
// Merged content is coming.
// 1. Store the merged content to the storage
if (!await this.core.databaseFileAccess.storeContent(path, p)) {
if (!(await this.core.databaseFileAccess.storeContent(path, p))) {
this._log(`Merged content cannot be stored:${path}`, LOG_LEVEL_NOTICE);
return MISSING_OR_ERROR;
}
@@ -65,13 +79,17 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
const isBinary = !isPlainText(path);
const alwaysNewer = this.settings.resolveConflictsByNewerFile;
if (isSame || isBinary || alwaysNewer) {
const result = compareMTime(leftLeaf.mtime, rightLeaf.mtime)
const result = compareMTime(leftLeaf.mtime, rightLeaf.mtime);
let loser = leftLeaf;
// if (lMtime > rMtime) {
if (result != TARGET_IS_NEW) {
loser = rightLeaf;
}
const subTitle = [`${isSame ? "same" : ""}`, `${isBinary ? "binary" : ""}`, `${alwaysNewer ? "alwaysNewer" : ""}`].join(",");
const subTitle = [
`${isSame ? "same" : ""}`,
`${isBinary ? "binary" : ""}`,
`${alwaysNewer ? "alwaysNewer" : ""}`,
].join(",");
return await this.core.$$resolveConflictByDeletingRev(path, loser.rev, subTitle);
}
// make diff.
@@ -86,20 +104,22 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
};
}
async $$resolveConflict(filename: FilePathWithPrefix): Promise<void> {
// const filename = filenames[0];
return await serialized(`conflict-resolve:${filename}`, async () => {
const conflictCheckResult = await this.checkConflictAndPerformAutoMerge(filename);
if (conflictCheckResult === MISSING_OR_ERROR || conflictCheckResult === NOT_CONFLICTED || conflictCheckResult === CANCELLED) {
if (
conflictCheckResult === MISSING_OR_ERROR ||
conflictCheckResult === NOT_CONFLICTED ||
conflictCheckResult === CANCELLED
) {
// nothing to do.
this._log(`conflict:Nothing to do:${filename}`);
return;
}
if (conflictCheckResult === AUTO_MERGED) {
//auto resolved, but need check again;
if (this.settings.syncAfterMerge && !this.core.suspended) {
if (this.settings.syncAfterMerge && !this.core.$$isSuspended()) {
//Wait for the running replication, if not running replication, run it once.
await this.core.$$waitForReplicationOnce();
}
@@ -110,7 +130,10 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
if (this.settings.showMergeDialogOnlyOnActive) {
const af = this.core.$$getActiveFilePath();
if (af && af != filename) {
this._log(`${filename} is conflicted. Merging process has been postponed to the file have got opened.`, LOG_LEVEL_NOTICE);
this._log(
`${filename} is conflicted. Merging process has been postponed to the file have got opened.`,
LOG_LEVEL_NOTICE
);
return;
}
}
@@ -124,18 +147,26 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
if (revs.length == 0) {
return Promise.resolve(true);
}
const mTimeAndRev = (await Promise.all(revs.map(async (rev) => {
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
if (leaf == false) {
return [0, rev] as [number, string];
}
return [leaf.mtime, rev] as [number, string];
}))).sort((a, b) => b[0] - a[0]);
this._log(`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`);
const mTimeAndRev = (
await Promise.all(
revs.map(async (rev) => {
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
if (leaf == false) {
return [0, rev] as [number, string];
}
return [leaf.mtime, rev] as [number, string];
})
)
).sort((a, b) => b[0] - a[0]);
this._log(
`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`
);
for (let i = 1; i < mTimeAndRev.length; i++) {
this._log(`conflict: Deleting the older revision ${mTimeAndRev[i][1]} (${new Date(mTimeAndRev[i][0]).toLocaleString()}) of ${filename}`);
this._log(
`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");
}
return true;
}
}
}

View File

@@ -1,11 +1,16 @@
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { normalizePath } from "../../deps.ts";
import { FLAGMD_REDFLAG, FLAGMD_REDFLAG2, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3, FLAGMD_REDFLAG3_HR } from "../../lib/src/common/types.ts";
import {
FLAGMD_REDFLAG,
FLAGMD_REDFLAG2,
FLAGMD_REDFLAG2_HR,
FLAGMD_REDFLAG3,
FLAGMD_REDFLAG3_HR,
} from "../../lib/src/common/types.ts";
import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleRedFlag extends AbstractModule implements ICoreModule {
async isFlagFileExist(path: string) {
const redflag = await this.core.storageAccess.isExists(normalizePath(path));
if (redflag) {
@@ -26,9 +31,11 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule {
}
}
isRedFlagRaised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG)
isRedFlag2Raised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG2) || await this.isFlagFileExist(FLAGMD_REDFLAG2_HR)
isRedFlag3Raised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG3) || await this.isFlagFileExist(FLAGMD_REDFLAG3_HR)
isRedFlagRaised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG);
isRedFlag2Raised = async () =>
(await this.isFlagFileExist(FLAGMD_REDFLAG2)) || (await this.isFlagFileExist(FLAGMD_REDFLAG2_HR));
isRedFlag3Raised = async () =>
(await this.isFlagFileExist(FLAGMD_REDFLAG3)) || (await this.isFlagFileExist(FLAGMD_REDFLAG3_HR));
async deleteRedFlag2() {
await this.deleteFlagFile(FLAGMD_REDFLAG2);
@@ -47,14 +54,24 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule {
if (isRedFlagRaised || isRedFlag2Raised || isRedFlag3Raised) {
if (isRedFlag2Raised) {
if (await this.core.confirm.askYesNoDialog("Rebuild everything has been scheduled! Are you sure to rebuild everything?", { defaultOption: "Yes", timeout: 0 }) !== "yes") {
if (
(await this.core.confirm.askYesNoDialog(
"Rebuild everything has been scheduled! Are you sure to rebuild everything?",
{ defaultOption: "Yes", timeout: 0 }
)) !== "yes"
) {
await this.deleteRedFlag2();
await this.core.$$performRestart();
return false;
}
}
if (isRedFlag3Raised) {
if (await this.core.confirm.askYesNoDialog("Fetch again has been scheduled! Are you sure?", { defaultOption: "Yes", timeout: 0 }) !== "yes") {
if (
(await this.core.confirm.askYesNoDialog("Fetch again has been scheduled! Are you sure?", {
defaultOption: "Yes",
timeout: 0,
})) !== "yes"
) {
await this.deleteRedFlag3();
await this.core.$$performRestart();
return false;
@@ -66,33 +83,63 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule {
this.settings.suspendFileWatching = true;
await this.saveSettings();
if (isRedFlag2Raised) {
this._log(`${FLAGMD_REDFLAG2} or ${FLAGMD_REDFLAG2_HR} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`, LOG_LEVEL_NOTICE);
this._log(
`${FLAGMD_REDFLAG2} or ${FLAGMD_REDFLAG2_HR} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`,
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") {
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 makeLocalChunkBeforeSync = ((await this.core.confirm.askYesNoDialog("Do you want to create local chunks before fetching?", { defaultOption: "Yes" })) == "yes");
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 makeLocalChunkBeforeSync =
(await this.core.confirm.askYesNoDialog(
`Do you want to create local chunks before fetching?
> [!MORE]-
> If creating local chunks before fetching, only the difference between the local and remote will be fetched.
`,
{ defaultOption: "Yes", title: "Trick to transfer efficiently" }
)) == "yes";
await this.core.rebuilder.$fetchLocal(makeLocalChunkBeforeSync);
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") {
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 {
// Case of FLAGMD_REDFLAG.
this.settings.writeLogToTheFile = true;
// await this.plugin.openDatabase();
const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured.";
const warningMessage =
"The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured.";
this._log(warningMessage, LOG_LEVEL_NOTICE);
}
}
@@ -102,5 +149,4 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule {
}
return true;
}
}
}

View File

@@ -13,4 +13,4 @@ export class ModuleRemoteGovernor extends AbstractModule implements ICoreModule
async $$markRemoteResolved(): Promise<void> {
return await this.core.replicator.markRemoteResolved(this.settings);
}
}
}

View File

@@ -1,6 +1,12 @@
import { Logger, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
import { extractObject } from "octagonal-wheels/object";
import { TweakValuesShouldMatchedTemplate, CompatibilityBreakingTweakValues, confName, type TweakValues } from "../../lib/src/common/types.ts";
import {
TweakValuesShouldMatchedTemplate,
CompatibilityBreakingTweakValues,
confName,
type TweakValues,
type RemoteDBSettings,
} from "../../lib/src/common/types.ts";
import { escapeMarkdownValue } from "../../lib/src/common/utils.ts";
import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
@@ -38,11 +44,13 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I
table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`;
}
const additionalMessage = rebuildRequired ? `
const additionalMessage = rebuildRequired
? `
**Note**: We have detected that some of the values are different to make incompatible the local database with the remote database.
If you choose to use the configured values, the local database will be rebuilt, and if you choose to use the values of this device, the remote database will be rebuilt.
Both of them takes a few minutes. Please choose after considering the situation.` : "";
Both of them takes a few minutes. Please choose after considering the situation.`
: "";
const message = `
Your configuration has not been matched with the one on the remote server.
@@ -67,9 +75,16 @@ Please select which one you want to use.
const CHOICE_AND_VALUES = [
[CHOICE_USE_REMOTE, preferred],
[CHOICE_USR_MINE, true],
[CHOICE_DISMISS, false]]
[CHOICE_DISMISS, false],
];
const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record<string, TweakValues | boolean>;
const retKey = await this.core.confirm.confirmWithMessage("Tweaks Mismatched or Changed", message, Object.keys(CHOICES), CHOICE_DISMISS, 60);
const retKey = await this.core.confirm.confirmWithMessage(
"Tweaks Mismatched or Changed",
message,
Object.keys(CHOICES),
CHOICE_DISMISS,
60
);
if (!retKey) return "IGNORE";
const conf = CHOICES[retKey];
@@ -78,7 +93,10 @@ Please select which one you want to use.
if (rebuildRequired) {
await this.core.rebuilder.$rebuildRemote();
}
Logger(`Tweak values on the remote server have been updated. Your other device will see this message.`, LOG_LEVEL_NOTICE);
Logger(
`Tweak values on the remote server have been updated. Your other device will see this message.`,
LOG_LEVEL_NOTICE
);
return "CHECKAGAIN";
}
if (conf) {
@@ -92,6 +110,87 @@ Please select which one you want to use.
return "CHECKAGAIN";
}
return "IGNORE";
}
}
async $$checkAndAskUseRemoteConfiguration(
trialSetting: RemoteDBSettings
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
const replicator = await this.core.$anyNewReplicator(trialSetting);
if (await replicator.tryConnectRemote(trialSetting)) {
const preferred = await replicator.getRemotePreferredTweakValues(trialSetting);
if (preferred) {
const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false;
// Making tables:
let table = `| Value name | This device | Stored | \n` + `|: --- |: ---- :|: ---- :| \n`;
let differenceCount = 0;
// const items = [mine,preferred]
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const valuePreferred = escapeMarkdownValue(preferred[key]);
const currentDisp = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])} |`;
if ((trialSetting as TweakValues)?.[key] !== preferred[key]) {
if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) {
rebuildRequired = true;
}
} else {
continue;
}
table += `| ${confName(key)} | ${currentDisp} ${valuePreferred} | \n`;
differenceCount++;
}
if (differenceCount === 0) {
this._log(
"The settings in the remote database are the same as the local database.",
LOG_LEVEL_NOTICE
);
return { result: false, requireFetch: false };
}
const additionalMessage =
rebuildRequired && this.core.settings.isConfigured
? `
>[!WARNING]
> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required.
***Please ensure that you have time and are connected to a stable network to apply!***`
: "";
const message = `
The settings in the remote database are as follows.
If you want to use these settings, please select "Use configured".
If you want to keep the settings of this device, please select "Dismiss".
${table}
>[!TIP]
> If you want to synchronise all settings, please use \`Sync settings via markdown\` after applying minimal configuration with this feature.
${additionalMessage}`;
const CHOICE_USE_REMOTE = "Use configured";
const CHOICE_DISMISS = "Dismiss";
// const CHOICE_AND_VALUES = [
// [CHOICE_USE_REMOTE, preferred],
// [CHOICE_DISMISS, false]]
const CHOICES = [CHOICE_USE_REMOTE, CHOICE_DISMISS];
const retKey = await this.core.confirm.askSelectStringDialogue(message, CHOICES, {
title: "Use Remote Configuration",
timeout: 0,
defaultAction: CHOICE_DISMISS,
});
if (!retKey) return { result: false, requireFetch: false };
if (retKey === CHOICE_DISMISS) return { result: false, requireFetch: false };
if (retKey === CHOICE_USE_REMOTE) {
return { result: { ...trialSetting, ...preferred }, requireFetch: rebuildRequired };
}
} else {
this._log("Failed to get the preferred tweak values from the remote server.", LOG_LEVEL_NOTICE);
}
return { result: false, requireFetch: false };
} else {
this._log("Failed to connect to the remote server.", LOG_LEVEL_NOTICE);
return { result: false, requireFetch: false };
}
}
}

View File

@@ -2,13 +2,20 @@ import { normalizePath, TFile, TFolder, type ListedFiles } from "obsidian";
import { SerializedFileAccess } from "./storageLib/SerializedFileAccess";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import type { FilePath, FilePathWithPrefix, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXStat } from "../../lib/src/common/types";
import type {
FilePath,
FilePathWithPrefix,
UXDataWriteOptions,
UXFileInfo,
UXFileInfoStub,
UXFolderInfo,
UXStat,
} from "../../lib/src/common/types";
import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "./storageLib/utilObsidian.ts";
import { StorageEventManagerObsidian, type StorageEventManager } from "./storageLib/StorageEventManager";
import type { StorageAccess } from "../interfaces/StorageAccess";
import { createBlob } from "../../lib/src/common/utils";
export class ModuleFileAccessObsidian extends AbstractObsidianModule implements IObsidianModule, StorageAccess {
vaultAccess!: SerializedFileAccess;
vaultManager: StorageEventManager = new StorageEventManagerObsidian(this.plugin, this.core);
@@ -43,12 +50,17 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
return this.vaultAccess.isStorageInsensitive();
}
$$shouldCheckCaseInsensitive(): boolean {
if (this.$$isStorageInsensitive()) return false;
return !this.settings.handleFilenameCaseSensitive;
}
async writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean> {
const file = this.vaultAccess.getAbstractFileByPath(path);
if (file instanceof TFile) {
return this.vaultAccess.vaultModify(file, data, opt);
} else if (file === null) {
return await this.vaultAccess.vaultCreate(path, data, opt) instanceof TFile;
return (await this.vaultAccess.vaultCreate(path, data, opt)) instanceof TFile;
} else {
this._log(`Could not write file (Possibly already exists as a folder): ${path}`, LOG_LEVEL_VERBOSE);
return false;
@@ -85,7 +97,6 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
}
async appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise<boolean> {
try {
await this.vaultAccess.adapterAppend(path, data, opt);
return true;
} catch (e) {
@@ -102,12 +113,11 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
ctime: file.stat.ctime,
mtime: file.stat.mtime,
size: file.stat.size,
type: "file"
type: "file",
});
} else {
throw new Error(`Could not stat file (Possibly does not exist): ${path}`);
}
}
statHidden(path: string): Promise<UXStat | null> {
return this.vaultAccess.adapterStat(path);
@@ -115,6 +125,9 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
async removeHidden(path: string): Promise<boolean> {
try {
await this.vaultAccess.adapterRemove(path);
if (this.vaultAccess.adapterStat(path) !== null) {
return false;
}
return true;
} catch (e) {
this._log(`Could not remove hidden file: ${path}`, LOG_LEVEL_VERBOSE);
@@ -132,7 +145,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
return await this.vaultAccess.adapterReadBinary(path);
}
async isExistsIncludeHidden(path: string): Promise<boolean> {
return await this.vaultAccess.adapterStat(path) !== null;
return (await this.vaultAccess.adapterStat(path)) !== null;
}
async ensureDir(path: string): Promise<boolean> {
try {
@@ -172,8 +185,8 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
const data = await this.vaultAccess.vaultReadAuto(file);
return {
...stub,
body: createBlob(data)
}
body: createBlob(data),
};
}
getStub(path: string): UXFileInfoStub | UXFolderInfo | null {
const file = this.vaultAccess.getAbstractFileByPath(path);
@@ -185,10 +198,10 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
return null;
}
getFiles(): UXFileInfoStub[] {
return this.vaultAccess.getFiles().map(f => TFileToUXFileInfoStub(f));
return this.vaultAccess.getFiles().map((f) => TFileToUXFileInfoStub(f));
}
getFileNames(): FilePath[] {
return this.vaultAccess.getFiles().map(f => f.path as FilePath);
return this.vaultAccess.getFiles().map((f) => f.path as FilePath);
}
async getFilesIncludeHidden(
@@ -205,20 +218,20 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
this._log(ex, LOG_LEVEL_VERBOSE);
return [];
}
skipFolder = skipFolder.map(e => e.toLowerCase());
skipFolder = skipFolder.map((e) => e.toLowerCase());
let files = [] as string[];
for (const file of w.files) {
if (excludeFilter && excludeFilter.some(ee => file.match(ee))) {
if (excludeFilter && excludeFilter.some((ee) => file.match(ee))) {
// If excludeFilter and includeFilter are both set, the file will be included in the list.
if (includeFilter) {
if (!includeFilter.some(e => file.match(e))) continue;
if (!includeFilter.some((e) => file.match(e))) continue;
} else {
continue;
}
}
if (includeFilter) {
if (!includeFilter.some(e => file.match(e))) continue;
if (!includeFilter.some((e) => file.match(e))) continue;
}
if (await this.plugin.$$isIgnoredByIgnoreFiles(file)) continue;
files.push(file);
@@ -226,19 +239,19 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
for (const v of w.folders) {
const folderName = (v.split("/").pop() ?? "").toLowerCase();
if (skipFolder.some(e => folderName === e)) {
continue
if (skipFolder.some((e) => folderName === e)) {
continue;
}
if (excludeFilter && excludeFilter.some(e => v.match(e))) {
if (excludeFilter && excludeFilter.some((e) => v.match(e))) {
if (includeFilter) {
if (!includeFilter.some(e => v.match(e))) {
if (!includeFilter.some((e) => v.match(e))) {
continue;
}
}
}
if (includeFilter) {
if (!includeFilter.some(e => v.match(e))) continue;
if (!includeFilter.some((e) => v.match(e))) continue;
}
// OK, deep dive!
files = files.concat(await this.getFilesIncludeHidden(v, includeFilter, excludeFilter, skipFolder));
@@ -250,7 +263,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
this.vaultAccess.touch(path as FilePath);
}
recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean {
const xFile = typeof file === "string" ? this.vaultAccess.getAbstractFileByPath(file) as TFile : file;
const xFile = typeof file === "string" ? (this.vaultAccess.getAbstractFileByPath(file) as TFile) : file;
if (xFile === null) return false;
if (xFile instanceof TFolder) return false;
return this.vaultAccess.recentlyTouched(xFile);
@@ -295,7 +308,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
async _deleteVaultItem(file: TFile | TFolder) {
if (file instanceof TFile) {
if (!await this.core.$$isTargetFile(file.path)) return;
if (!(await this.core.$$isTargetFile(file.path))) return;
}
const dir = file.parent;
if (this.settings.trashInsteadDelete) {
@@ -308,7 +321,9 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
this._log(`files: ${dir.children.length}`);
if (dir.children.length == 0) {
if (!this.settings.doNotDeleteFolder) {
this._log(`All files under the parent directory (${dir.path}) have been deleted, so delete this one.`);
this._log(
`All files under the parent directory (${dir.path}) have been deleted, so delete this one.`
);
await this._deleteVaultItem(dir);
}
}
@@ -323,4 +338,4 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
return await this._deleteVaultItem(file);
}
}
}
}

View File

@@ -1,15 +1,20 @@
import { AbstractObsidianModule, type IObsidianModule } from '../AbstractObsidianModule.ts';
import { scheduleTask } from 'octagonal-wheels/concurrency/task';
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from '../../common/utils.ts';
import { askSelectString, askString, askYesNo, confirmWithMessage, confirmWithMessageWithWideButton } from './UILib/dialogs.ts';
import { Notice } from '../../deps.ts';
import type { Confirm } from '../interfaces/Confirm.ts';
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from "../../common/utils.ts";
import {
askSelectString,
askString,
askYesNo,
confirmWithMessage,
confirmWithMessageWithWideButton,
} from "./UILib/dialogs.ts";
import { Notice } from "../../deps.ts";
import type { Confirm } from "../interfaces/Confirm.ts";
// This module cannot be a common module because it depends on Obsidian's API.
// However, we have to make compatible one for other platform.
export class ModuleInputUIObsidian extends AbstractObsidianModule implements IObsidianModule, Confirm {
$everyOnload(): Promise<boolean> {
this.core.confirm = this;
return Promise.resolve(true);
@@ -22,8 +27,18 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
return askString(this.app, title, key, placeholder, isPassword);
}
async askYesNoDialog(message: string, opt: { title?: string, defaultOption?: "Yes" | "No", timeout?: number } = { title: "Confirmation" }): Promise<"yes" | "no"> {
const ret = await confirmWithMessageWithWideButton(this.plugin, opt.title || "Confirmation", message, ["Yes", "No"], opt.defaultOption ?? "No", opt.timeout);
async askYesNoDialog(
message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" }
): Promise<"yes" | "no"> {
const ret = await confirmWithMessageWithWideButton(
this.plugin,
opt.title || "Confirmation",
message,
["Yes", "No"],
opt.defaultOption ?? "No",
opt.timeout
);
return ret == "Yes" ? "yes" : "no";
}
@@ -31,8 +46,19 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
return askSelectString(this.app, message, items);
}
askSelectStringDialogue(message: string, buttons: string[], opt: { title?: string, defaultAction: (typeof buttons)[number], timeout?: number }): Promise<(typeof buttons)[number] | false> {
return confirmWithMessageWithWideButton(this.plugin, opt.title || "Select", message, buttons, opt.defaultAction, opt.timeout);
askSelectStringDialogue(
message: string,
buttons: string[],
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number }
): Promise<(typeof buttons)[number] | false> {
return confirmWithMessageWithWideButton(
this.plugin,
opt.title || "Select",
message,
buttons,
opt.defaultAction,
opt.timeout
);
}
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) {
@@ -40,9 +66,11 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
const [beforeText, afterText] = dialogText.split("{HERE}", 2);
doc.createEl("span", undefined, (a) => {
a.appendText(beforeText);
a.appendChild(a.createEl("a", undefined, (anchor) => {
anchorCallback(anchor);
}));
a.appendChild(
a.createEl("a", undefined, (anchor) => {
anchorCallback(anchor);
})
);
a.appendText(afterText);
});
});
@@ -55,8 +83,7 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
}
scheduleTask(popupKey + "-close", 20000, () => {
const popup = retrieveMemoObject<Notice>(popupKey);
if (!popup)
return;
if (!popup) return;
if (popup?.noticeEl?.isShown()) {
popup.hide();
}
@@ -64,8 +91,13 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
});
});
}
confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false> {
confirmWithMessage(
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout?: number
): Promise<(typeof buttons)[number] | false> {
return confirmWithMessage(this.plugin, title, contentMd, buttons, defaultAction, timeout);
}
}
}

View File

@@ -1,7 +1,29 @@
import { ButtonComponent } from "obsidian";
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts";
import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts";
import { delay } from "octagonal-wheels/promises";
export class InputStringDialog extends Modal {
class AutoClosableModal extends Modal {
removeEvent: (() => void) | undefined;
constructor(app: App) {
super(app);
this.removeEvent = eventHub.onEvent(EVENT_PLUGIN_UNLOADED, async () => {
await delay(100);
if (!this.removeEvent) return;
this.close();
this.removeEvent = undefined;
});
}
onClose() {
if (this.removeEvent) {
this.removeEvent();
this.removeEvent = undefined;
}
}
}
export class InputStringDialog extends AutoClosableModal {
result: string | false = false;
onSubmit: (result: string | false) => void;
title: string;
@@ -10,7 +32,14 @@ export class InputStringDialog extends Modal {
isManuallyClosed = false;
isPassword = false;
constructor(app: App, title: string, key: string, placeholder: string, isPassword: boolean, onSubmit: (result: string | false) => void) {
constructor(
app: App,
title: string,
key: string,
placeholder: string,
isPassword: boolean,
onSubmit: (result: string | false) => void
) {
super(app);
this.onSubmit = onSubmit;
this.title = title;
@@ -23,30 +52,36 @@ export class InputStringDialog extends Modal {
const { contentEl } = this;
this.titleEl.setText(this.title);
const formEl = contentEl.createDiv();
new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) =>
text.onChange((value) => {
this.result = value;
})
);
new Setting(formEl).addButton((btn) =>
btn
.setButtonText("Ok")
.setCta()
.onClick(() => {
this.isManuallyClosed = true;
this.close();
new Setting(formEl)
.setName(this.key)
.setClass(this.isPassword ? "password-input" : "normal-input")
.addText((text) =>
text.onChange((value) => {
this.result = value;
})
).addButton((btn) =>
btn
.setButtonText("Cancel")
.setCta()
.onClick(() => {
this.close();
})
);
);
new Setting(formEl)
.addButton((btn) =>
btn
.setButtonText("Ok")
.setCta()
.onClick(() => {
this.isManuallyClosed = true;
this.close();
})
)
.addButton((btn) =>
btn
.setButtonText("Cancel")
.setCta()
.onClick(() => {
this.close();
})
);
}
onClose() {
super.onClose();
const { contentEl } = this;
contentEl.empty();
if (this.isManuallyClosed) {
@@ -58,13 +93,18 @@ export class InputStringDialog extends Modal {
}
export class PopoverSelectString extends FuzzySuggestModal<string> {
app: App;
callback: ((e: string) => void) | undefined = () => { };
callback: ((e: string) => void) | undefined = () => {};
getItemsFun: () => string[] = () => {
return ["yes", "no"];
};
}
constructor(app: App, note: string, placeholder: string | undefined, getItemsFun: (() => string[]) | undefined, callback: (e: string) => void) {
constructor(
app: App,
note: string,
placeholder: string | undefined,
getItemsFun: (() => string[]) | undefined,
callback: (e: string) => void
) {
super(app);
this.app = app;
this.setPlaceholder((placeholder ?? "y/n) ") + note);
@@ -95,8 +135,7 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
}
}
export class MessageBox extends Modal {
export class MessageBox extends AutoClosableModal {
plugin: Plugin;
title: string;
contentMd: string;
@@ -111,7 +150,16 @@ export class MessageBox extends Modal {
onSubmit: (result: string | false) => void;
constructor(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout: number | undefined, wideButton: boolean, onSubmit: (result: (typeof buttons)[number] | false) => void) {
constructor(
plugin: Plugin,
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout: number | undefined,
wideButton: boolean,
onSubmit: (result: (typeof buttons)[number] | false) => void
) {
super(plugin.app);
this.plugin = plugin;
this.title = title;
@@ -144,16 +192,19 @@ export class MessageBox extends Modal {
onOpen() {
const { contentEl } = this;
this.titleEl.setText(this.title);
contentEl.addEventListener("click", () => {
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
this.defaultButtonComponent?.setButtonText(`${this.defaultAction}`);
}
})
const div = contentEl.createDiv();
div.style.userSelect = "text";
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
const buttonSetting = new Setting(contentEl);
const labelWrapper = contentEl.createDiv();
labelWrapper.addClass("sls-dialogue-note-wrapper");
const labelEl = labelWrapper.createEl("label", { text: "To stop the countdown, tap anywhere on the dialogue" });
labelEl.addClass("sls-dialogue-note-countdown");
if (!this.timeout || !this.timer) {
labelWrapper.empty();
labelWrapper.style.display = "none";
}
buttonSetting.infoEl.style.display = "none";
buttonSetting.controlEl.style.flexWrap = "wrap";
if (this.wideButton) {
@@ -162,19 +213,26 @@ export class MessageBox extends Modal {
buttonSetting.controlEl.style.justifyContent = "center";
buttonSetting.controlEl.style.flexGrow = "1";
}
contentEl.addEventListener("click", () => {
if (this.timer) {
labelWrapper.empty();
labelWrapper.style.display = "none";
clearInterval(this.timer);
this.timer = undefined;
this.defaultButtonComponent?.setButtonText(`${this.defaultAction}`);
}
});
for (const button of this.buttons) {
buttonSetting.addButton((btn) => {
btn
.setButtonText(button)
.onClick(() => {
this.isManuallyClosed = true;
this.result = button;
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
this.close();
})
btn.setButtonText(button).onClick(() => {
this.isManuallyClosed = true;
this.result = button;
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
this.close();
});
if (button == this.defaultAction) {
this.defaultButtonComponent = btn;
btn.setCta();
@@ -184,12 +242,12 @@ export class MessageBox extends Modal {
btn.buttonEl.style.width = "100%";
}
return btn;
}
)
});
}
}
onClose() {
super.onClose();
const { contentEl } = this;
contentEl.empty();
if (this.timer) {
@@ -204,25 +262,42 @@ export class MessageBox extends Modal {
}
}
export function confirmWithMessage(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false> {
export function confirmWithMessage(
plugin: Plugin,
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout?: number
): Promise<(typeof buttons)[number] | false> {
return new Promise((res) => {
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, false, (result) => res(result));
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, false, (result) =>
res(result)
);
dialog.open();
});
}
export function confirmWithMessageWithWideButton(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false> {
export function confirmWithMessageWithWideButton(
plugin: Plugin,
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout?: number
): Promise<(typeof buttons)[number] | false> {
return new Promise((res) => {
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, true, (result) => res(result));
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, true, (result) =>
res(result)
);
dialog.open();
});
}
export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
return new Promise((res) => {
const popover = new PopoverSelectString(app, message, undefined, undefined, (result) => res(result as "yes" | "no"));
const popover = new PopoverSelectString(app, message, undefined, undefined, (result) =>
res(result as "yes" | "no")
);
popover.open();
});
};
@@ -235,11 +310,15 @@ export const askSelectString = (app: App, message: string, items: string[]): Pro
});
};
export const askString = (app: App, title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> => {
export const askString = (
app: App,
title: string,
key: string,
placeholder: string,
isPassword: boolean = false
): Promise<string | false> => {
return new Promise((res) => {
const dialog = new InputStringDialog(app, title, key, placeholder, isPassword, (result) => res(result));
dialog.open();
});
};

View File

@@ -9,7 +9,7 @@ import { markChangesAreSame } from "../../../common/utils.ts";
import { type UXFileInfo } from "../../../lib/src/common/types.ts";
function getFileLockKey(file: TFile | TFolder | string | UXFileInfo) {
return `fl:${typeof (file) == "string" ? file : file.path}`;
return `fl:${typeof file == "string" ? file : file.path}`;
}
function toArrayBuffer(arr: Uint8Array | ArrayBuffer | DataView): ArrayBufferLike {
if (arr instanceof Uint8Array) {
@@ -35,9 +35,9 @@ async function processWriteFile<T>(file: TFile | TFolder | string | UXFileInfo,
}
export class SerializedFileAccess {
app: App
plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }>
constructor(app: App, plugin: typeof this["plugin"]) {
app: App;
plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }>;
constructor(app: App, plugin: (typeof this)["plugin"]) {
this.app = app;
this.plugin = plugin;
}
@@ -72,10 +72,12 @@ export class SerializedFileAccess {
async adapterWrite(file: TFile | string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) {
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));
} else {
return await processWriteFile(file, () => this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options));
return await processWriteFile(file, () =>
this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options)
);
}
}
@@ -97,19 +99,17 @@ export class SerializedFileAccess {
return await processReadFile(file, () => this.app.vault.readBinary(file));
}
async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) {
if (typeof (data) === "string") {
if (typeof data === "string") {
return await processWriteFile(file, async () => {
const oldData = await this.app.vault.read(file);
if (data === oldData) {
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
return true;
}
await this.app.vault.modify(file, data, options)
await this.app.vault.modify(file, data, options);
return true;
}
);
});
} else {
return await processWriteFile(file, async () => {
const oldData = await this.app.vault.readBinary(file);
@@ -117,13 +117,17 @@ export class SerializedFileAccess {
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
return true;
}
await this.app.vault.modifyBinary(file, toArrayBuffer(data), options)
await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
return true;
});
}
}
async vaultCreate(path: string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions): Promise<TFile> {
if (typeof (data) === "string") {
async vaultCreate(
path: string,
data: string | ArrayBuffer | Uint8Array,
options?: DataWriteOptions
): Promise<TFile> {
if (typeof data === "string") {
return await processWriteFile(path, () => this.app.vault.create(path, data, options));
} else {
return await processWriteFile(path, () => this.app.vault.createBinary(path, toArrayBuffer(data), options));
@@ -135,7 +139,7 @@ export class SerializedFileAccess {
}
async adapterAppend(normalizedPath: string, data: string, options?: DataWriteOptions) {
return await this.app.vault.adapter.append(normalizedPath, data, options)
return await this.app.vault.adapter.append(normalizedPath, data, options);
}
async delete(file: TFile | TFolder, force = false) {
@@ -145,8 +149,6 @@ export class SerializedFileAccess {
return await processWriteFile(file, () => this.app.vault.trash(file, force));
}
isStorageInsensitive(): boolean {
//@ts-ignore
return this.app.vault.adapter.insensitive ?? true;
@@ -188,22 +190,23 @@ export class SerializedFileAccess {
}
}
touchedFiles: string[] = [];
touch(file: TFile | FilePath) {
const f = file instanceof TFile ? file : this.getAbstractFileByPath(file) as TFile;
const f = file instanceof TFile ? file : (this.getAbstractFileByPath(file) as TFile);
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
this.touchedFiles.unshift(key);
this.touchedFiles = this.touchedFiles.slice(0, 100);
}
recentlyTouched(file: TFile | InternalFileInfo | UXFileInfoStub) {
const key = "stat" in file ? `${file.path}-${file.stat.mtime}-${file.stat.size}` : `${file.path}-${file.mtime}-${file.size}`;
const key =
"stat" in file
? `${file.path}-${file.stat.mtime}-${file.stat.size}`
: `${file.path}-${file.mtime}-${file.size}`;
if (this.touchedFiles.indexOf(key) == -1) return false;
return true;
}
clearTouched() {
this.touchedFiles = [];
}
}
}

View File

@@ -1,18 +1,32 @@
import { TAbstractFile, TFile, TFolder } from "../../../deps.ts";
import { Logger } from "../../../lib/src/common/logger.ts";
import { shouldBeIgnored } from "../../../lib/src/string_and_binary/path.ts";
import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, type FilePath, type FilePathWithPrefix, type UXFileInfoStub, type UXInternalFileInfoStub } from "../../../lib/src/common/types.ts";
import {
DEFAULT_SETTINGS,
LOG_LEVEL_DEBUG,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
type FilePath,
type FilePathWithPrefix,
type UXFileInfoStub,
type UXInternalFileInfoStub,
} from "../../../lib/src/common/types.ts";
import { delay, fireAndForget } from "../../../lib/src/common/utils.ts";
import { type FileEventItem, type FileEventType } from "../../../common/types.ts";
import { serialized, skipIfDuplicated } from "../../../lib/src/concurrency/lock.ts";
import { finishAllWaitingForTimeout, finishWaitingForTimeout, isWaitingForTimeout, waitForTimeout } from "../../../lib/src/concurrency/task.ts";
import {
finishAllWaitingForTimeout,
finishWaitingForTimeout,
isWaitingForTimeout,
waitForTimeout,
} from "../../../lib/src/concurrency/task.ts";
import { Semaphore } from "../../../lib/src/concurrency/semaphore.ts";
import type { LiveSyncCore } from "../../../main.ts";
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "./utilObsidian.ts";
import ObsidianLiveSyncPlugin from "../../../main.ts";
// import { InternalFileToUXFileInfo } from "../platforms/obsidian.ts";
export type FileEvent = {
type: FileEventType;
file: UXFileInfoStub | UXInternalFileInfoStub;
@@ -21,30 +35,26 @@ export type FileEvent = {
skipBatchWait?: boolean;
};
export abstract class StorageEventManager {
abstract beginWatch(): void;
abstract flushQueue(): void;
abstract appendQueue(items: FileEvent[], ctx?: any): Promise<void>;
abstract cancelQueue(key: string): void;
abstract isWaiting(filename: FilePath): boolean;
}
export class StorageEventManagerObsidian extends StorageEventManager {
plugin: ObsidianLiveSyncPlugin;
core: LiveSyncCore;
get shouldBatchSave() {
return this.plugin.shouldBatchSave;
return this.core.settings?.batchSave && this.core.settings?.liveSync != true;
}
get batchSaveMinimumDelay(): number {
return this.plugin.batchSaveMinimumDelay;
return this.core.settings?.batchSaveMinimumDelay ?? DEFAULT_SETTINGS.batchSaveMinimumDelay;
}
get batchSaveMaximumDelay(): number {
return this.plugin.batchSaveMaximumDelay
return this.core.settings?.batchSaveMaximumDelay ?? DEFAULT_SETTINGS.batchSaveMaximumDelay;
}
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
super();
@@ -83,10 +93,11 @@ export class StorageEventManagerObsidian extends StorageEventManager {
}
const data = info?.data as string;
const fi: FileEvent = {
type: "CHANGED", file: TFileToUXFileInfoStub(file), cachedData: data,
}
void this.appendQueue([
fi])
type: "CHANGED",
file: TFileToUXFileInfoStub(file),
cachedData: data,
};
void this.appendQueue([fi]);
}
watchVaultCreate(file: TAbstractFile, ctx?: any) {
@@ -109,17 +120,27 @@ export class StorageEventManagerObsidian extends StorageEventManager {
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
if (file instanceof TFile) {
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue([
{
type: "DELETE", file: {
path: oldFile as FilePath, name: file.name, stat: {
mtime: file.stat.mtime,
ctime: file.stat.ctime,
size: file.stat.size,
type: "file"
}, deleted: true
}, skipBatchWait: true
}, { type: "CREATE", file: fileInfo, skipBatchWait: true },], ctx);
void this.appendQueue(
[
{
type: "DELETE",
file: {
path: oldFile as FilePath,
name: file.name,
stat: {
mtime: file.stat.mtime,
ctime: file.stat.ctime,
size: file.stat.size,
type: "file",
},
deleted: true,
},
skipBatchWait: true,
},
{ type: "CREATE", file: fileInfo, skipBatchWait: true },
],
ctx
);
}
}
// Watch raw events (Internal API)
@@ -142,47 +163,51 @@ export class StorageEventManagerObsidian extends StorageEventManager {
if (!path.startsWith(this.plugin.app.vault.configDir)) return;
const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "")
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
if (ignorePatterns.some(e => path.match(e))) return;
.split(",")
.filter((e) => e)
.map((e) => new RegExp(e, "i"));
if (ignorePatterns.some((e) => path.match(e))) return;
if (path.endsWith("/")) {
// Folder
// Folder
return;
}
void this.appendQueue([
{
type: "INTERNAL", file: InternalFileToUXFileInfoStub(path),
}], null);
void this.appendQueue(
[
{
type: "INTERNAL",
file: InternalFileToUXFileInfoStub(path),
},
],
null
);
}
// Cache file and waiting to can be proceed.
async appendQueue(params: FileEvent[], ctx?: any) {
if (!this.plugin.settings.isConfigured) return;
if (this.plugin.settings.suspendFileWatching) return;
this.plugin.totalFileEventCount++;
if (!this.core.settings.isConfigured) return;
if (this.core.settings.suspendFileWatching) return;
this.core.$$markFileListPossiblyChanged();
// Flag up to be reload
const processFiles = new Set<FilePath>();
for (const param of params) {
if (shouldBeIgnored(param.file.path)) {
continue;
}
const atomicKey = [
0,
0,
0,
0,
0,
0].map(e => `${Math.floor(Math.random() * 100000)}`).join("-");
const atomicKey = [0, 0, 0, 0, 0, 0].map((e) => `${Math.floor(Math.random() * 100000)}`).join("-");
const type = param.type;
const file = param.file;
const oldPath = param.oldPath;
if (type !== "INTERNAL") {
const size = (file as UXFileInfoStub).stat.size;
if (this.plugin.$$isFileSizeExceeded(size) && (type == "CREATE" || type == "CHANGED")) {
Logger(`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`, LOG_LEVEL_NOTICE);
if (this.core.$$isFileSizeExceeded(size) && (type == "CREATE" || type == "CHANGED")) {
Logger(
`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`,
LOG_LEVEL_NOTICE
);
continue;
}
}
if (file instanceof TFolder) continue;
if (!await this.plugin.$$isTargetFile(file.path)) continue;
if (!(await this.core.$$isTargetFile(file.path))) continue;
// Stop cache using to prevent the corruption;
// let cache: null | string | ArrayBuffer;
@@ -190,20 +215,26 @@ export class StorageEventManagerObsidian extends StorageEventManager {
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
// Wait for a bit while to let the writer has marked `touched` at the file.
await delay(10);
if (this.plugin.storageAccess.recentlyTouched(file)) {
if (this.core.storageAccess.recentlyTouched(file)) {
continue;
}
}
let cache: string | undefined = undefined;
if (param.cachedData) {
cache = param.cachedData
cache = param.cachedData;
}
this.enqueue({
type, args: {
file: file, oldPath, cache, ctx,
}, skipBatchWait: param.skipBatchWait, key: atomicKey
})
type,
args: {
file: file,
oldPath,
cache,
ctx,
},
skipBatchWait: param.skipBatchWait,
key: atomicKey,
});
processFiles.add(file.path as FilePath);
if (oldPath) {
processFiles.add(oldPath as FilePath);
@@ -238,7 +269,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
Logger(`Processing ${filename}: Started`, LOG_LEVEL_DEBUG);
let noMoreFiles = false;
do {
const target = this.bufferedQueuedItems.find(e => e.args.file.path == filename);
const target = this.bufferedQueuedItems.find((e) => e.args.file.path == filename);
if (target === undefined) {
noMoreFiles = true;
break;
@@ -251,7 +282,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
// }
const type = target.type;
if (target.cancelled) {
Logger(`Processing ${filename}: Cancelled (scheduled): ${operationType}`, LOG_LEVEL_DEBUG)
Logger(`Processing ${filename}: Cancelled (scheduled): ${operationType}`, LOG_LEVEL_DEBUG);
this.cancelStandingBy(target);
continue;
}
@@ -261,19 +292,31 @@ export class StorageEventManagerObsidian extends StorageEventManager {
let canWait = true;
const now = Date.now();
if (waitedSince !== undefined) {
if (waitedSince + (this.batchSaveMaximumDelay * 1000) < now) {
Logger(`Processing ${filename}: Could not wait no more: ${operationType}`, LOG_LEVEL_INFO)
if (waitedSince + this.batchSaveMaximumDelay * 1000 < now) {
Logger(
`Processing ${filename}: Could not wait no more: ${operationType}`,
LOG_LEVEL_INFO
);
canWait = false;
}
}
if (canWait) {
if (waitedSince === undefined) this.waitedSince.set(filename, now)
target.batched = true
Logger(`Processing ${filename}: Waiting for batch save delay: ${operationType}`, LOG_LEVEL_DEBUG)
if (waitedSince === undefined) this.waitedSince.set(filename, now);
target.batched = true;
Logger(
`Processing ${filename}: Waiting for batch save delay: ${operationType}`,
LOG_LEVEL_DEBUG
);
this.updateStatus();
const result = await waitForTimeout(`storage-event-manager-batchsave-${filename}`, this.batchSaveMinimumDelay * 1000);
const result = await waitForTimeout(
`storage-event-manager-batchsave-${filename}`,
this.batchSaveMinimumDelay * 1000
);
if (!result) {
Logger(`Processing ${filename}: Cancelled by new queue: ${operationType}`, LOG_LEVEL_DEBUG)
Logger(
`Processing ${filename}: Cancelled by new queue: ${operationType}`,
LOG_LEVEL_DEBUG
);
// If could not wait for the timeout, possibly we got a new queue. therefore, currently processing one should be cancelled
this.cancelStandingBy(target);
continue;
@@ -281,16 +324,19 @@ export class StorageEventManagerObsidian extends StorageEventManager {
}
}
} else {
Logger(`Processing ${filename}:Requested to perform immediately ${filename}: ${operationType}`, LOG_LEVEL_DEBUG)
Logger(
`Processing ${filename}:Requested to perform immediately ${filename}: ${operationType}`,
LOG_LEVEL_DEBUG
);
}
Logger(`Processing ${filename}: Request main to process: ${operationType}`, LOG_LEVEL_DEBUG)
Logger(`Processing ${filename}: Request main to process: ${operationType}`, LOG_LEVEL_DEBUG);
await this.requestProcessQueue(target);
} while (!noMoreFiles)
} while (!noMoreFiles);
} finally {
release()
release();
}
Logger(`Processing ${filename}: Finished`, LOG_LEVEL_DEBUG);
})
});
}
cancelStandingBy(fei: FileEventItem) {
@@ -302,30 +348,30 @@ export class StorageEventManagerObsidian extends StorageEventManager {
try {
this.processingCount++;
this.bufferedQueuedItems.remove(fei);
this.updateStatus()
this.updateStatus();
this.waitedSince.delete(fei.args.file.path);
await this.handleFileEvent(fei);
} finally {
this.processingCount--;
this.updateStatus()
this.updateStatus();
}
}
isWaiting(filename: FilePath) {
return isWaitingForTimeout(`storage-event-manager-batchsave-${filename}`);
}
flushQueue() {
this.bufferedQueuedItems.forEach(e => e.skipBatchWait = true)
this.bufferedQueuedItems.forEach((e) => (e.skipBatchWait = true));
finishAllWaitingForTimeout("storage-event-manager-batchsave-", true);
}
cancelQueue(key: string) {
this.bufferedQueuedItems.forEach(e => {
if (e.key === key) e.skipBatchWait = true
})
this.bufferedQueuedItems.forEach((e) => {
if (e.key === key) e.skipBatchWait = true;
});
}
updateStatus() {
const allItems = this.bufferedQueuedItems.filter(e => !e.cancelled)
const batchedCount = allItems.filter(e => e.batched && !e.skipBatchWait).length;
this.core.batched.value = batchedCount
const allItems = this.bufferedQueuedItems.filter((e) => !e.cancelled);
const batchedCount = allItems.filter((e) => e.batched && !e.skipBatchWait).length;
this.core.batched.value = batchedCount;
this.core.processing.value = this.processingCount;
this.core.totalQueued.value = allItems.length - batchedCount;
}
@@ -336,23 +382,26 @@ export class StorageEventManagerObsidian extends StorageEventManager {
return await serialized(lockKey, async () => {
// TODO CHECK
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 == "INTERNAL" || file.isInternal) {
await this.plugin.$anyProcessOptionalFileEvent(file.path as unknown as FilePath);
await this.core.$anyProcessOptionalFileEvent(file.path as unknown as FilePath);
} else {
// let mtime = file.stat.mtime;
if (queue.type == "DELETE") {
await this.plugin.$anyHandlerProcessesFileEvent(queue);
await this.core.$anyHandlerProcessesFileEvent(queue);
} else {
if (file.stat.mtime == last) {
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE);
// Should Cancel the relative operations? (e.g. rename)
// Should Cancel the relative operations? (e.g. rename)
// this.cancelRelativeEvent(queue);
return;
}
if (!await this.plugin.$anyHandlerProcessesFileEvent(queue)) {
Logger(`STORAGE -> DB: Handler failed, cancel the relative operations: ${file.path}`, LOG_LEVEL_INFO);
// cancel running queues and remove one of atomic operation (e.g. rename)
if (!(await this.core.$anyHandlerProcessesFileEvent(queue))) {
Logger(
`STORAGE -> DB: Handler failed, cancel the relative operations: ${file.path}`,
LOG_LEVEL_INFO
);
// cancel running queues and remove one of atomic operation (e.g. rename)
this.cancelRelativeEvent(queue);
return;
}
@@ -364,4 +413,4 @@ export class StorageEventManagerObsidian extends StorageEventManager {
cancelRelativeEvent(item: FileEventItem): void {
this.cancelQueue(item.key);
}
}
}

View File

@@ -6,10 +6,22 @@ import type { SerializedFileAccess } from "./SerializedFileAccess.ts";
import { addPrefix, isPlainText } from "../../../lib/src/string_and_binary/path.ts";
import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
import { createBlob } from "../../../lib/src/common/utils.ts";
import type { FilePath, FilePathWithPrefix, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub } from "../../../lib/src/common/types.ts";
import type {
FilePath,
FilePathWithPrefix,
UXFileInfo,
UXFileInfoStub,
UXFolderInfo,
UXInternalFileInfoStub,
} from "../../../lib/src/common/types.ts";
import type { LiveSyncCore } from "../../../main.ts";
export async function TFileToUXFileInfo(core: LiveSyncCore, file: TFile, prefix?: string, deleted?: boolean): Promise<UXFileInfo> {
export async function TFileToUXFileInfo(
core: LiveSyncCore,
file: TFile,
prefix?: string,
deleted?: boolean
): Promise<UXFileInfo> {
const isPlain = isPlainText(file.name);
const possiblyLarge = !isPlain;
let content: Blob;
@@ -34,11 +46,14 @@ export async function TFileToUXFileInfo(core: LiveSyncCore, file: TFile, prefix?
type: "file",
},
body: content,
}
};
}
export async function InternalFileToUXFileInfo(fullPath: string, vaultAccess: SerializedFileAccess, prefix: string = ICHeader): Promise<UXFileInfo> {
export async function InternalFileToUXFileInfo(
fullPath: string,
vaultAccess: SerializedFileAccess,
prefix: string = ICHeader
): Promise<UXFileInfo> {
const name = fullPath.split("/").pop() as string;
const stat = await vaultAccess.adapterStat(fullPath);
if (stat == null) throw new Error(`File not found: ${fullPath}`);
@@ -64,7 +79,7 @@ export async function InternalFileToUXFileInfo(fullPath: string, vaultAccess: Se
type: "file",
},
body: content,
}
};
}
export function TFileToUXFileInfoStub(file: TFile | TAbstractFile, deleted?: boolean): UXFileInfoStub {
@@ -81,8 +96,8 @@ export function TFileToUXFileInfoStub(file: TFile | TAbstractFile, deleted?: boo
ctime: file.stat.ctime,
type: "file",
},
deleted: deleted
}
deleted: deleted,
};
return ret;
}
export function InternalFileToUXFileInfoStub(filename: FilePathWithPrefix, deleted?: boolean): UXInternalFileInfoStub {
@@ -93,8 +108,8 @@ export function InternalFileToUXFileInfoStub(filename: FilePathWithPrefix, delet
isFolder: false,
stat: undefined,
isInternal: true,
deleted
}
deleted,
};
return ret;
}
export function TFolderToUXFileInfoStub(file: TFolder): UXFolderInfo {
@@ -103,7 +118,7 @@ export function TFolderToUXFileInfoStub(file: TFolder): UXFolderInfo {
path: file.path as FilePathWithPrefix,
parent: file.parent?.path as FilePath | undefined,
isFolder: true,
children: file.children.map(e => TFileToUXFileInfoStub(e)),
}
children: file.children.map((e) => TFileToUXFileInfoStub(e)),
};
return ret;
}
}

View File

@@ -3,22 +3,35 @@ import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { throttle } from "octagonal-wheels/function";
import { eventHub } from "../../common/events.ts";
import { BASE_IS_NEW, compareFileFreshness, EVEN, getPath, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
import { type FilePathWithPrefixLC, type FilePathWithPrefix, type MetaEntry, isMetaEntry, type EntryDoc, LOG_LEVEL_VERBOSE, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, type UXFileInfoStub } from "../../lib/src/common/types.ts";
import {
type FilePathWithPrefixLC,
type FilePathWithPrefix,
type MetaEntry,
isMetaEntry,
type EntryDoc,
LOG_LEVEL_VERBOSE,
LOG_LEVEL_NOTICE,
LOG_LEVEL_INFO,
LOG_LEVEL_DEBUG,
type UXFileInfoStub,
} from "../../lib/src/common/types.ts";
import { isAnyNote } from "../../lib/src/common/utils.ts";
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleInitializerFile extends AbstractModule implements ICoreModule {
async $$performFullScan(showingNotice?: boolean): Promise<void> {
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.
if (!this.settings.isConfigured) {
if (showingNotice) {
this._log("LiveSync is not configured yet. Synchronising between the storage and the local database is now prevented.", LOG_LEVEL_NOTICE, "syncAll");
this._log(
"LiveSync is not configured yet. Synchronising between the storage and the local database is now prevented.",
LOG_LEVEL_NOTICE,
"syncAll"
);
}
return;
}
@@ -47,22 +60,25 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
return path as FilePathWithPrefixLC;
}
return (path as string).toLowerCase() as FilePathWithPrefixLC;
}
};
// If handleFilenameCaseSensitive is enabled, `FilePathWithPrefixLC` is the same as `FilePathWithPrefix`.
const storageFileNameMap = Object.fromEntries(_filesStorage.map((e) => [
e.path, e] as [FilePathWithPrefix, UXFileInfoStub]));
const storageFileNameMap = Object.fromEntries(
_filesStorage.map((e) => [e.path, e] as [FilePathWithPrefix, UXFileInfoStub])
);
const storageFileNames = Object.keys(storageFileNameMap) as FilePathWithPrefix[];
const storageFileNameCapsPair = storageFileNames.map((e) => [
e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC]);
const storageFileNameCapsPair = storageFileNames.map(
(e) => [e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC]
);
// const storageFileNameCS2CI = Object.fromEntries(storageFileNameCapsPair) as Record<FilePathWithPrefix, FilePathWithPrefixLC>;
const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map(e => [
e[1], e[0]])) as Record<FilePathWithPrefixLC, FilePathWithPrefix>;
const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map((e) => [e[1], e[0]])) as Record<
FilePathWithPrefixLC,
FilePathWithPrefix
>;
this._log("Collecting local files on the DB", LOG_LEVEL_VERBOSE);
const _DBEntries = [] as MetaEntry[];
@@ -70,10 +86,15 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
let count = 0;
for await (const doc of this.localDatabase.findAllNormalDocs()) {
count++;
if (count % 25 == 0) this._log(`Collecting local files on the DB: ${count}`, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll");
if (count % 25 == 0)
this._log(
`Collecting local files on the DB: ${count}`,
showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO,
"syncAll"
);
const path = getPath(doc);
if (isValidPath(path) && await this.core.$$isTargetFile(path, true)) {
if (isValidPath(path) && (await this.core.$$isTargetFile(path, true))) {
if (!isMetaEntry(doc)) {
this._log(`Invalid entry: ${path}`, LOG_LEVEL_INFO);
continue;
@@ -82,24 +103,28 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
}
}
const databaseFileNameMap = Object.fromEntries(_DBEntries.map((e) => [
getPath(e), e] as [FilePathWithPrefix, MetaEntry]));
const databaseFileNameMap = Object.fromEntries(
_DBEntries.map((e) => [getPath(e), e] as [FilePathWithPrefix, MetaEntry])
);
const databaseFileNames = Object.keys(databaseFileNameMap) as FilePathWithPrefix[];
const databaseFileNameCapsPair = databaseFileNames.map((e) => [
e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC]);
const databaseFileNameCapsPair = databaseFileNames.map(
(e) => [e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC]
);
// const databaseFileNameCS2CI = Object.fromEntries(databaseFileNameCapsPair) as Record<FilePathWithPrefix, FilePathWithPrefixLC>;
const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map(e => [
e[1], e[0]])) as Record<FilePathWithPrefix, FilePathWithPrefixLC>;
const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map((e) => [e[1], e[0]])) as Record<
FilePathWithPrefix,
FilePathWithPrefixLC
>;
const allFiles = unique([
...Object.keys(databaseFileNameCI2CS),
...Object.keys(storageFileNameCI2CS)]) as FilePathWithPrefixLC[];
...Object.keys(storageFileNameCI2CS),
]) as FilePathWithPrefixLC[];
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: ${allFiles.length}`, LOG_LEVEL_VERBOSE, "syncAll");
const filesExistOnlyInStorage = allFiles.filter((e) => !databaseFileNameCI2CS[e]);
const filesExistOnlyInDatabase = allFiles.filter((e) => !storageFileNameCI2CS[e]);
const filesExistBoth = allFiles.filter((e) => databaseFileNameCI2CS[e] && storageFileNameCI2CS[e]);
@@ -128,92 +153,110 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
let success = 0;
let failed = 0;
const step = 10;
const processor = new QueueProcessor(async (e) => {
try {
await callback(e[0]);
success++;
// return
} catch (ex) {
this._log(`Error while ${procedureName}`, LOG_LEVEL_NOTICE);
this._log(ex, LOG_LEVEL_VERBOSE);
failed++;
}
if ((success + failed) % step == 0) {
const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${processor._queue.length}`;
updateLog(procedureName, msg);
}
return;
}, {
batchSize: 1,
concurrentLimit: 10,
delay: 0,
suspended: true,
maintainDelay: false,
interval: 0
}, objects)
const processor = new QueueProcessor(
async (e) => {
try {
await callback(e[0]);
success++;
// return
} catch (ex) {
this._log(`Error while ${procedureName}`, LOG_LEVEL_NOTICE);
this._log(ex, LOG_LEVEL_VERBOSE);
failed++;
}
if ((success + failed) % step == 0) {
const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${processor._queue.length}`;
updateLog(procedureName, msg);
}
return;
},
{
batchSize: 1,
concurrentLimit: 10,
delay: 0,
suspended: true,
maintainDelay: false,
interval: 0,
},
objects
);
await processor.waitForAllDoneAndTerminate();
const msg = `${procedureName} All done: DONE:${success}, FAILED:${failed}`;
updateLog(procedureName, msg)
}
initProcess.push(runAll("UPDATE DATABASE", filesExistOnlyInStorage, async (e) => {
// console.warn("UPDATE DATABASE", e);
const file = storageFileNameMap[storageFileNameCI2CS[e]];
if (!this.core.$$isFileSizeExceeded(file.stat.size)) {
const path = file.path;
await this.core.fileHandler.storeFileToDB(file);
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(path, true));
eventHub.emitEvent("event-file-changed", { file: path, automated: true });
} else {
this._log(`UPDATE DATABASE: ${e} has been skipped due to file size exceeding the limit`, logLevel);
}
}));
initProcess.push(runAll("UPDATE STORAGE", filesExistOnlyInDatabase, async (e) => {
const w = databaseFileNameMap[databaseFileNameCI2CS[e]];
const path = getPath(w) ?? e;
if (w && !(w.deleted || w._deleted)) {
if (!this.core.$$isFileSizeExceeded(w.size)) {
// await this.pullFile(path, undefined, false, undefined, false);
// Memo: No need to force
await this.core.fileHandler.dbToStorage(path, null, true);
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(e, true));
eventHub.emitEvent("event-file-changed", {
file: e, automated: true
});
this._log(`Check or pull from db:${path} OK`);
updateLog(procedureName, msg);
};
initProcess.push(
runAll("UPDATE DATABASE", filesExistOnlyInStorage, async (e) => {
// console.warn("UPDATE DATABASE", e);
const file = storageFileNameMap[storageFileNameCI2CS[e]];
if (!this.core.$$isFileSizeExceeded(file.stat.size)) {
const path = file.path;
await this.core.fileHandler.storeFileToDB(file);
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(path, true));
eventHub.emitEvent("event-file-changed", { file: path, automated: true });
} else {
this._log(`UPDATE STORAGE: ${path} has been skipped due to file size exceeding the limit`, logLevel);
this._log(`UPDATE DATABASE: ${e} has been skipped due to file size exceeding the limit`, logLevel);
}
} else if (w) {
this._log(`Deletion history skipped: ${path}`, LOG_LEVEL_VERBOSE);
} else {
this._log(`entry not found: ${path}`);
}
}));
})
);
initProcess.push(
runAll("UPDATE STORAGE", filesExistOnlyInDatabase, async (e) => {
const w = databaseFileNameMap[databaseFileNameCI2CS[e]];
const path = getPath(w) ?? e;
if (w && !(w.deleted || w._deleted)) {
if (!this.core.$$isFileSizeExceeded(w.size)) {
// await this.pullFile(path, undefined, false, undefined, false);
// Memo: No need to force
await this.core.fileHandler.dbToStorage(path, null, true);
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(e, true));
eventHub.emitEvent("event-file-changed", {
file: e,
automated: true,
});
this._log(`Check or pull from db:${path} OK`);
} else {
this._log(
`UPDATE STORAGE: ${path} has been skipped due to file size exceeding the limit`,
logLevel
);
}
} else if (w) {
this._log(`Deletion history skipped: ${path}`, LOG_LEVEL_VERBOSE);
} else {
this._log(`entry not found: ${path}`);
}
})
);
const fileMap = filesExistBoth.map(path => {
const fileMap = filesExistBoth.map((path) => {
const file = storageFileNameMap[storageFileNameCI2CS[path]];
const doc = databaseFileNameMap[databaseFileNameCI2CS[path]];
return { file, doc }
})
initProcess.push(runAll("SYNC DATABASE AND STORAGE", fileMap, async (e) => {
const { file, doc } = e;
if (!this.core.$$isFileSizeExceeded(file.stat.size) && !this.core.$$isFileSizeExceeded(doc.size)) {
await this.syncFileBetweenDBandStorage(file, doc);
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(getPath(doc), true));
eventHub.emitEvent("event-file-changed", {
file: getPath(doc), automated: true
});
} else {
this._log(`SYNC DATABASE AND STORAGE: ${getPath(doc)} has been skipped due to file size exceeding the limit`, logLevel);
}
}))
return { file, doc };
});
initProcess.push(
runAll("SYNC DATABASE AND STORAGE", fileMap, async (e) => {
const { file, doc } = e;
if (!this.core.$$isFileSizeExceeded(file.stat.size) && !this.core.$$isFileSizeExceeded(doc.size)) {
await this.syncFileBetweenDBandStorage(file, doc);
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(getPath(doc), true));
eventHub.emitEvent("event-file-changed", {
file: getPath(doc),
automated: true,
});
} else {
this._log(
`SYNC DATABASE AND STORAGE: ${getPath(doc)} has been skipped due to file size exceeding the limit`,
logLevel
);
}
})
);
await Promise.all(initProcess);
// this.setStatusBarText(`NOW TRACKING!`);
this._log("Initialized, NOW TRACKING!");
if (!isInitialized) {
await (this.core.kvDB.set("initialized", true))
await this.core.kvDB.set("initialized", true);
}
if (showingNotice) {
this._log("Initialize done!", LOG_LEVEL_NOTICE, "syncAll");
@@ -222,14 +265,14 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
async syncFileBetweenDBandStorage(file: UXFileInfoStub, doc: MetaEntry) {
if (!doc) {
throw new Error(`Missing doc:${(file as any).path}`)
throw new Error(`Missing doc:${(file as any).path}`);
}
if ("path" in file) {
const w = this.core.storageAccess.getFileStub((file as any).path);
if (w) {
file = w;
} else {
throw new Error(`Missing file:${(file as any).path}`)
throw new Error(`Missing file:${(file as any).path}`);
}
}
@@ -240,21 +283,28 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
this._log("STORAGE -> DB :" + file.path);
await this.core.fileHandler.storeFileToDB(file);
eventHub.emitEvent("event-file-changed", {
file: file.path, automated: true
file: file.path,
automated: true,
});
} else {
this._log(`STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
this._log(
`STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`,
LOG_LEVEL_NOTICE
);
}
break;
case TARGET_IS_NEW:
if (!this.core.$$isFileSizeExceeded(doc.size)) {
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))) {
this._log(`STORAGE <- DB : Cloud not read ${file.path}, possibly deleted`, LOG_LEVEL_NOTICE);
}
return caches;
} else {
this._log(`STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
this._log(
`STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`,
LOG_LEVEL_NOTICE
);
}
break;
case EVEN:
@@ -263,31 +313,29 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
default:
this._log("STORAGE ?? DB :" + file.path + " Something got weird");
}
}
// This method uses an old version of database accessor, which is not recommended.
// TODO: Fix
async collectDeletedFiles() {
const limitDays = this.settings.automaticallyDeleteMetadataOfDeletedFiles;
if (limitDays <= 0) return;
this._log(`Checking expired file history`);
const limit = Date.now() - (86400 * 1000 * limitDays);
const limit = Date.now() - 86400 * 1000 * limitDays;
const notes: {
path: string,
mtime: number,
ttl: number,
doc: PouchDB.Core.ExistingDocument<EntryDoc & PouchDB.Core.AllDocsMeta>
path: string;
mtime: number;
ttl: number;
doc: PouchDB.Core.ExistingDocument<EntryDoc & PouchDB.Core.AllDocsMeta>;
}[] = [];
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (isAnyNote(doc)) {
if (doc.deleted && (doc.mtime - limit) < 0) {
if (doc.deleted && doc.mtime - limit < 0) {
notes.push({
path: getPath(doc),
mtime: doc.mtime,
ttl: (doc.mtime - limit) / 1000 / 86400,
doc: doc
doc: doc,
});
}
}
@@ -307,22 +355,22 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule
}
async $$initializeDatabase(showingNotice: boolean = false, reopenDatabase = true): Promise<boolean> {
this.core.isReady = false;
if ((!reopenDatabase) || await this.core.$$openDatabase()) {
this.core.$$resetIsReady();
if (!reopenDatabase || (await this.core.$$openDatabase())) {
if (this.localDatabase.isReady) {
await this.core.$$performFullScan(showingNotice);
}
if (!await this.core.$everyOnDatabaseInitialized(showingNotice)) {
if (!(await this.core.$everyOnDatabaseInitialized(showingNotice))) {
this._log(`Initializing database has been failed on some module`, LOG_LEVEL_NOTICE);
return false;
}
this.core.isReady = true;
this.core.$$markIsReady();
// run queued event once.
await this.core.$everyCommitPendingFileEvent();
return true;
} else {
this.core.isReady = false;
this.core.$$resetIsReady();
return false;
}
}
}
}

View File

@@ -6,7 +6,6 @@ import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
tryCloseKvDB() {
try {
this.core.kvDB?.close();
@@ -42,7 +41,7 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
}
async $everyOnloadAfterLoadSettings(): Promise<boolean> {
if (!await this.openKeyValueDB()) {
if (!(await this.openKeyValueDB())) {
return false;
}
this.core.simpleStore = this.core.$$getSimpleStore<any>("os");
@@ -60,11 +59,21 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
delete: async (key: string): Promise<void> => {
await this.core.kvDB.del(`${prefix}${key}`);
},
keys: async (from: string | undefined, to: string | undefined, count?: number | undefined): Promise<string[]> => {
const ret = this.core.kvDB.keys(IDBKeyRange.bound(`${prefix}${from || ""}`, `${prefix}${to || ""}`), count);
return (await ret).map(e => e.toString()).filter(e => e.startsWith(prefix)).map(e => e.substring(prefix.length));
}
}
keys: async (
from: string | undefined,
to: string | undefined,
count?: number | undefined
): Promise<string[]> => {
const ret = this.core.kvDB.keys(
IDBKeyRange.bound(`${prefix}${from || ""}`, `${prefix}${to || ""}`),
count
);
return (await ret)
.map((e) => e.toString())
.filter((e) => e.startsWith(prefix))
.map((e) => e.substring(prefix.length));
},
};
}
$everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return this.openKeyValueDB();
@@ -72,7 +81,7 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
async $everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
try {
const kvDBKey = "queued-files"
const kvDBKey = "queued-files";
await this.core.kvDB.del(kvDBKey);
// localStorage.removeItem(lsKey);
await this.core.kvDB.destroy();
@@ -87,4 +96,4 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule {
}
return true;
}
}
}

View File

@@ -1,14 +1,23 @@
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from 'octagonal-wheels/common/logger.js';
import { SETTING_VERSION_SUPPORT_CASE_INSENSITIVE } from '../../lib/src/common/types.js';
import { EVENT_REQUEST_OPEN_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI, eventHub } from '../../common/events.ts';
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js";
import { SETTING_VERSION_SUPPORT_CASE_INSENSITIVE } from "../../lib/src/common/types.js";
import {
EVENT_REQUEST_OPEN_SETTING_WIZARD,
EVENT_REQUEST_OPEN_SETTINGS,
EVENT_REQUEST_OPEN_SETUP_URI,
eventHub,
} from "../../common/events.ts";
import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleMigration extends AbstractModule implements ICoreModule {
const URI_DOC = "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use";
export class ModuleMigration extends AbstractModule implements ICoreModule {
async migrateDisableBulkSend() {
if (this.settings.sendChunksBulk) {
this._log("Send chunks in bulk has been enabled, however, this feature had been corrupted. Sorry for your inconvenience. Automatically disabled.", LOG_LEVEL_NOTICE);
this._log(
"Send chunks in bulk has been enabled, however, this feature had been corrupted. Sorry for your inconvenience. Automatically disabled.",
LOG_LEVEL_NOTICE
);
this.settings.sendChunksBulk = false;
this.settings.sendChunksBulkMaxSize = 1;
await this.saveSettings();
@@ -18,20 +27,27 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
const old = this.settings.settingVersion;
const current = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE;
// Check each migrations(old -> current)
if (!await this.migrateToCaseInsensitive(old, current)) {
if (!(await this.migrateToCaseInsensitive(old, current))) {
this._log(`Migration failed or cancelled from ${old} to ${current}`, LOG_LEVEL_NOTICE);
return;
}
}
async migrateToCaseInsensitive(old: number, current: number) {
if (this.settings.handleFilenameCaseSensitive !== undefined && this.settings.doNotUseFixedRevisionForChunks !== undefined) {
if (
this.settings.handleFilenameCaseSensitive !== undefined &&
this.settings.doNotUseFixedRevisionForChunks !== undefined
) {
if (current < SETTING_VERSION_SUPPORT_CASE_INSENSITIVE) {
this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE;
await this.saveSettings();
}
return true;
}
if (old >= SETTING_VERSION_SUPPORT_CASE_INSENSITIVE && this.settings.handleFilenameCaseSensitive !== undefined && this.settings.doNotUseFixedRevisionForChunks !== undefined) {
if (
old >= SETTING_VERSION_SUPPORT_CASE_INSENSITIVE &&
this.settings.handleFilenameCaseSensitive !== undefined &&
this.settings.doNotUseFixedRevisionForChunks !== undefined
) {
return true;
}
@@ -41,9 +57,14 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
try {
const remoteInfo = await this.core.replicator.getRemotePreferredTweakValues(this.settings);
if (remoteInfo) {
remoteHandleFilenameCaseSensitive = "handleFilenameCaseSensitive" in remoteInfo ? remoteInfo.handleFilenameCaseSensitive : false;
remoteDoNotUseFixedRevisionForChunks = "doNotUseFixedRevisionForChunks" in remoteInfo ? remoteInfo.doNotUseFixedRevisionForChunks : false;
if (remoteHandleFilenameCaseSensitive !== undefined || remoteDoNotUseFixedRevisionForChunks !== undefined) {
remoteHandleFilenameCaseSensitive =
"handleFilenameCaseSensitive" in remoteInfo ? remoteInfo.handleFilenameCaseSensitive : false;
remoteDoNotUseFixedRevisionForChunks =
"doNotUseFixedRevisionForChunks" in remoteInfo ? remoteInfo.doNotUseFixedRevisionForChunks : false;
if (
remoteHandleFilenameCaseSensitive !== undefined ||
remoteDoNotUseFixedRevisionForChunks !== undefined
) {
remoteChecked = true;
}
} else {
@@ -77,7 +98,13 @@ ___Note2: The chunks are completely immutable, we can fetch only the metadata an
const OPTION_FETCH = "Yes, fetch again";
const DISMISS = "No, please ask again";
const options = [OPTION_FETCH, DISMISS];
const ret = await this.core.confirm.confirmWithMessage("Case Sensitivity", message, options, "No, please ask again", 40);
const ret = await this.core.confirm.confirmWithMessage(
"Case Sensitivity",
message,
options,
"No, please ask again",
40
);
if (ret == OPTION_FETCH) {
this.settings.handleFilenameCaseSensitive = remoteHandleFilenameCaseSensitive || false;
this.settings.doNotUseFixedRevisionForChunks = remoteDoNotUseFixedRevisionForChunks || false;
@@ -94,7 +121,6 @@ ___Note2: The chunks are completely immutable, we can fetch only the metadata an
} else {
return false;
}
}
const ENABLE_BOTH = "Enable both";
@@ -118,10 +144,7 @@ ___However, to enable either of these changes, both remote and local databases n
- If you do not have enough time, please choose \`${DISMISS}\`. You will be prompted again later.
- If you have rebuilt the database on another device, please select \`${DISMISS}\` and try synchronizing again. Since a difference has been detected, you will be prompted again.
`;
const options = [
ENABLE_BOTH,
ENABLE_FILENAME_CASE_INSENSITIVE,
ENABLE_FIXED_REVISION_FOR_CHUNKS];
const options = [ENABLE_BOTH, ENABLE_FILENAME_CASE_INSENSITIVE, ENABLE_FIXED_REVISION_FOR_CHUNKS];
if (remoteChecked) {
options.push(ADJUST_TO_REMOTE);
}
@@ -150,13 +173,64 @@ ___However, to enable either of these changes, both remote and local databases n
case DISMISS:
default:
return false;
}
this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE;
await this.saveSettings();
await this.core.rebuilder.scheduleRebuild();
await this.core.$$performRestart();
}
async initialMessage() {
const message = `Your device has **not been set up yet**. Let me guide you through the setup process.
Please keep in mind that every dialogue content can be copied to the clipboard. If you need to refer to it later, you can paste it into a note in Obsidian. You can also translate it into your language using a translation tool.
First, do you have **Setup URI**?
Note: If you do not know what it is, please refer to the [documentation](${URI_DOC}).
`;
const USE_SETUP = "Yes, I have";
const NEXT = "No, I do not have";
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_SETUP, NEXT], {
title: "Welcome to Self-hosted LiveSync",
defaultAction: USE_SETUP,
});
if (ret === USE_SETUP) {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI);
return false;
} else if (ret == NEXT) {
return true;
}
return false;
}
async askAgainForSetupURI() {
const message = `We strongly recommend that you generate a set-up URI and use it.
If you do not have knowledge about it, please refer to the [documentation](${URI_DOC}) (Sorry again, but it is important).
How do you want to set it up manually?`;
const USE_MINIMAL = "Take me into the setup wizard";
const USE_SETUP = "Set it up all manually";
const NEXT = "Remind me at the next launch";
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, NEXT], {
title: "Recommendation to use Setup URI",
defaultAction: USE_MINIMAL,
});
if (ret === USE_MINIMAL) {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD);
return false;
}
if (ret === USE_SETUP) {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTINGS);
return false;
} else if (ret == NEXT) {
return false;
}
return false;
}
async $everyOnFirstInitialize(): Promise<boolean> {
@@ -170,41 +244,14 @@ ___However, to enable either of these changes, both remote and local databases n
}
if (!this.settings.isConfigured) {
// Case sensitivity
const message = `Hello and welcome to Self-hosted LiveSync.
Your device seems to **not be configured yet**. Please finish the setup and synchronise your vaults!
Click anywhere to stop counting down.
## At the first device
- With Setup URI -> Use \`Use the copied setup URI\`.
If you have configured it automatically, you should have one.
- Without Setup URI -> Use \`Setup wizard\` in setting dialogue. **\`Minimal setup\` is recommended**.
- What is the Setup URI? -> Do not worry! We have [some docs](https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use) now. Please refer to them once.
## At the subsequent device
- With Setup URI -> Use \`Use the copied setup URI\`.
If you do not have it yet, you can copy it on the first device.
- Without Setup URI -> Use \`Setup wizard\` in setting dialogue, but **strongly recommends using setup URI**.
`
const OPEN_SETUP = "Open setting dialog";
const USE_SETUP = "Use the copied setup URI";
const DISMISS = "Dismiss";
const ret = await this.core.confirm.confirmWithMessage("Welcome to Self-hosted LiveSync", message, [
USE_SETUP, OPEN_SETUP, DISMISS], DISMISS, 40);
if (ret === OPEN_SETUP) {
try {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTINGS);
} catch (ex) {
this._log("Something went wrong on opening setting dialog, please open it manually", LOG_LEVEL_NOTICE);
this._log(ex, LOG_LEVEL_VERBOSE);
}
} else if (ret == USE_SETUP) {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI);
if (!(await this.initialMessage()) || !(await this.askAgainForSetupURI())) {
this._log(
"The setup has been cancelled, Self-hosted LiveSync waiting for your setup!",
LOG_LEVEL_NOTICE
);
return false;
}
return true;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
// This file is based on a file that was published by the @remotely-save, under the Apache 2 License.
// I would love to express my deepest gratitude to the original authors for their hard work and dedication. Without their contributions, this project would not have been possible.
//
//
// Original Implementation is here: https://github.com/remotely-save/remotely-save/blob/28b99557a864ef59c19d2ad96101196e401718f0/src/remoteForS3.ts
import {
FetchHttpHandler,
type FetchHttpHandlerOptions,
} from "@smithy/fetch-http-handler";
import { FetchHttpHandler, type FetchHttpHandlerOptions } from "@smithy/fetch-http-handler";
import { HttpRequest, HttpResponse, type HttpHandlerOptions } from "@smithy/protocol-http";
//@ts-ignore
import { requestTimeout } from "@smithy/fetch-http-handler/dist-es/request-timeout";
@@ -25,20 +22,13 @@ import { requestUrl, type RequestUrlParam } from "../../../deps.ts";
export class ObsHttpHandler extends FetchHttpHandler {
requestTimeoutInMs: number | undefined;
reverseProxyNoSignUrl: string | undefined;
constructor(
options?: FetchHttpHandlerOptions,
reverseProxyNoSignUrl?: string
) {
constructor(options?: FetchHttpHandlerOptions, reverseProxyNoSignUrl?: string) {
super(options);
this.requestTimeoutInMs =
options === undefined ? undefined : options.requestTimeout;
this.requestTimeoutInMs = options === undefined ? undefined : options.requestTimeout;
this.reverseProxyNoSignUrl = reverseProxyNoSignUrl;
}
// eslint-disable-next-line require-await
async handle(
request: HttpRequest,
{ abortSignal }: HttpHandlerOptions = {}
): Promise<{ response: HttpResponse }> {
async handle(request: HttpRequest, { abortSignal }: HttpHandlerOptions = {}): Promise<{ response: HttpResponse }> {
if (abortSignal?.aborted) {
const abortError = new Error("Request aborted");
abortError.name = "AbortError";
@@ -54,18 +44,13 @@ export class ObsHttpHandler extends FetchHttpHandler {
}
const { port, method } = request;
let url = `${request.protocol}//${request.hostname}${port ? `:${port}` : ""
}${path}`;
if (
this.reverseProxyNoSignUrl !== undefined &&
this.reverseProxyNoSignUrl !== ""
) {
let url = `${request.protocol}//${request.hostname}${port ? `:${port}` : ""}${path}`;
if (this.reverseProxyNoSignUrl !== undefined && this.reverseProxyNoSignUrl !== "") {
const urlObj = new URL(url);
urlObj.host = this.reverseProxyNoSignUrl;
url = urlObj.href;
}
const body =
method === "GET" || method === "HEAD" ? undefined : request.body;
const body = method === "GET" || method === "HEAD" ? undefined : request.body;
const transformedHeaders: Record<string, string> = {};
for (const key of Object.keys(request.headers)) {

View File

@@ -1,17 +1,22 @@
import { AbstractObsidianModule, type IObsidianModule } from '../AbstractObsidianModule.ts';
import { LOG_LEVEL_DEBUG, LOG_LEVEL_VERBOSE } from 'octagonal-wheels/common/logger';
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from '../../deps.ts';
import { type EntryDoc, type FilePathWithPrefix } from '../../lib/src/common/types.ts';
import { getPathFromTFile } from '../../common/utils.ts';
import { disableEncryption, enableEncryption, isCloudantURI, isValidRemoteCouchDBURI, replicationFilter } from '../../lib/src/pouchdb/utils_couchdb.ts';
import { setNoticeClass } from '../../lib/src/mock_and_interop/wrapper.ts';
import { ObsHttpHandler } from './APILib/ObsHttpHandler.ts';
import { PouchDB } from '../../lib/src/pouchdb/pouchdb-browser.ts';
import { reactive, reactiveSource } from 'octagonal-wheels/dataobject/reactive';
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_DEBUG, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts";
import { type EntryDoc, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
import { getPathFromTFile } from "../../common/utils.ts";
import {
disableEncryption,
enableEncryption,
isCloudantURI,
isValidRemoteCouchDBURI,
replicationFilter,
} from "../../lib/src/pouchdb/utils_couchdb.ts";
import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts";
import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts";
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts";
import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive";
setNoticeClass(Notice);
async function fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
const ret = await requestUrl(request);
if (ret.status - (ret.status % 100) !== 200) {
@@ -27,22 +32,35 @@ async function fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse>
}
export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidianModule {
_customHandler!: ObsHttpHandler;
authHeaderSource = reactiveSource<string>("");
authHeader = reactive(() =>
this.authHeaderSource.value == "" ? "" : "Basic " + window.btoa(this.authHeaderSource.value));
this.authHeaderSource.value == "" ? "" : "Basic " + window.btoa(this.authHeaderSource.value)
);
last_successful_post = false;
$$customFetchHandler(): ObsHttpHandler {
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
return this._customHandler;
}
$$getLastPostFailedBySize(): boolean {
return !this.last_successful_post;
}
async $$connectRemoteCouchDB(uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
async $$connectRemoteCouchDB(
uri: string,
auth: { username: string; password: string },
disableRequestURI: boolean,
passphrase: string | false,
useDynamicIterationCount: boolean,
performSetup: boolean,
skipInfo: boolean,
compression: boolean
): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
const userNameAndPassword = (auth.username && auth.password) ? `${auth.username}:${auth.password}` : "";
const userNameAndPassword = auth.username && auth.password ? `${auth.username}:${auth.password}` : "";
if (this.authHeaderSource.value != userNameAndPassword) {
this.authHeaderSource.value = userNameAndPassword;
}
@@ -62,7 +80,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
if (opts_length > 1000 * 1000 * 10) {
// over 10MB
if (isCloudantURI(uri)) {
this.plugin.last_successful_post = false;
this.last_successful_post = false;
this._log("This request should fail on IBM Cloudant.", LOG_LEVEL_VERBOSE);
throw new Error("This request should fail on IBM Cloudant.");
}
@@ -91,9 +109,9 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
const r = await fetchByAPI(requestParam);
if (method == "POST" || method == "PUT") {
this.plugin.last_successful_post = r.status - (r.status % 100) == 200;
this.last_successful_post = r.status - (r.status % 100) == 200;
} else {
this.plugin.last_successful_post = true;
this.last_successful_post = true;
}
this._log(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL_DEBUG);
@@ -106,7 +124,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
this._log(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
// limit only in bulk_docs.
if (url.toString().indexOf("_bulk_docs") !== -1) {
this.plugin.last_successful_post = false;
this.last_successful_post = false;
}
this._log(ex);
throw ex;
@@ -123,15 +141,17 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
const response: Response = await fetch(url, opts);
if (method == "POST" || method == "PUT") {
this.plugin.last_successful_post = response.ok;
this.last_successful_post = response.ok;
} else {
this.plugin.last_successful_post = true;
this.last_successful_post = true;
}
this._log(`HTTP:${method}${size} to:${localURL} -> ${response.status}`, LOG_LEVEL_DEBUG);
if (Math.floor(response.status / 100) !== 2) {
if (method != "GET" && localURL.indexOf("/_local/") === -1 && !localURL.endsWith("/")) {
const r = response.clone();
this._log(`The request may have failed. The reason sent by the server: ${r.status}: ${r.statusText}`);
this._log(
`The request may have failed. The reason sent by the server: ${r.status}: ${r.statusText}`
);
try {
this._log(await (await r.blob()).text(), LOG_LEVEL_VERBOSE);
@@ -140,7 +160,10 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
this._log(_, LOG_LEVEL_VERBOSE);
}
} else {
this._log(`Just checkpoint or some server information has been missing. The 404 error shown above is not an error.`, LOG_LEVEL_VERBOSE)
this._log(
`Just checkpoint or some server information has been missing. The 404 error shown above is not an error.`,
LOG_LEVEL_VERBOSE
);
}
}
return response;
@@ -148,7 +171,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
this._log(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
// limit only in bulk_docs.
if (url.toString().indexOf("_bulk_docs") !== -1) {
this.plugin.last_successful_post = false;
this.last_successful_post = false;
}
this._log(ex);
throw ex;
@@ -174,7 +197,8 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
} catch (ex: any) {
let msg = `${ex?.name}:${ex?.message}`;
if (ex?.name == "TypeError" && ex?.message == "Failed to fetch") {
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
msg +=
"\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
}
this._log(ex, LOG_LEVEL_VERBOSE);
return msg;
@@ -190,7 +214,10 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
return this.app.vault.getName();
}
$$getVaultName(): string {
return this.core.$$vaultName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
return (
this.core.$$vaultName() +
(this.settings?.additionalSuffixOfDatabaseName ? "-" + this.settings.additionalSuffixOfDatabaseName : "")
);
}
$$getActiveFilePath(): FilePathWithPrefix | undefined {
const file = this.app.workspace.getActiveFile();
@@ -201,7 +228,6 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
}
$anyGetAppId(): Promise<string | undefined> {
return Promise.resolve(`${("appId" in this.app ? this.app.appId : "")}`);
return Promise.resolve(`${"appId" in this.app ? this.app.appId : ""}`);
}
}
}

View File

@@ -1,25 +1,35 @@
import { AbstractObsidianModule, type IObsidianModule } from '../AbstractObsidianModule.ts';
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 { scheduleTask } from 'octagonal-wheels/concurrency/task';
import { type TFile } from '../../deps.ts';
import { fireAndForget } from 'octagonal-wheels/promises';
import { type FilePathWithPrefix } from '../../lib/src/common/types.ts';
import { reactive, reactiveSource } from 'octagonal-wheels/dataobject/reactive';
import { collectingChunks, pluginScanningCount, hiddenFilesEventCount, hiddenFilesProcessingCount } from '../../lib/src/mock_and_interop/stores.ts';
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
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 { scheduleTask } from "octagonal-wheels/concurrency/task";
import { type TFile } from "../../deps.ts";
import { fireAndForget } from "octagonal-wheels/promises";
import { type FilePathWithPrefix } from "../../lib/src/common/types.ts";
import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive";
import {
collectingChunks,
pluginScanningCount,
hiddenFilesEventCount,
hiddenFilesProcessingCount,
} from "../../lib/src/mock_and_interop/stores.ts";
export class ModuleObsidianEvents extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> {
// this.registerEvent(this.app.workspace.on("editor-change", ));
this.plugin.registerEvent(this.app.vault.on("rename", (file, oldPath) => {
eventHub.emitEvent(EVENT_FILE_RENAMED, { newPath: file.path, old: oldPath });
}));
this.plugin.registerEvent(this.app.workspace.on("active-leaf-change", () => eventHub.emitEvent(EVENT_LEAF_ACTIVE_CHANGED)));
this.plugin.registerEvent(
this.app.vault.on("rename", (file, oldPath) => {
eventHub.emitEvent(EVENT_FILE_RENAMED, {
newPath: file.path as FilePathWithPrefix,
old: oldPath as FilePathWithPrefix,
});
})
);
this.plugin.registerEvent(
this.app.workspace.on("active-leaf-change", () => eventHub.emitEvent(EVENT_LEAF_ACTIVE_CHANGED))
);
return Promise.resolve(true);
}
$$performRestart(): void {
this._performAppReload();
}
@@ -33,15 +43,13 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
swapSaveCommand() {
this._log("Modifying callback of the save command", LOG_LEVEL_VERBOSE);
const saveCommandDefinition = (this.app as any).commands?.commands?.[
"editor:save-file"
];
const saveCommandDefinition = (this.app as any).commands?.commands?.["editor:save-file"];
const save = saveCommandDefinition?.callback;
if (typeof save === "function") {
this.initialCallback = save;
saveCommandDefinition.callback = () => {
scheduleTask("syncOnEditorSave", 250, () => {
if (this.plugin._unloaded) {
if (this.core.$$isUnloaded()) {
this._log("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
saveCommandDefinition.callback = this.initialCallback;
this.initialCallback = undefined;
@@ -60,7 +68,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
//@ts-ignore
window.CodeMirrorAdapter.commands.save = () => {
//@ts-ignore
_this.app.commands.executeCommandById('editor:save-file')
_this.app.commands.executeCommandById("editor:save-file");
// _this.app.performCommand('editor:save-file');
};
}
@@ -98,14 +106,14 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
// TODO:FIXME AT V0.17.31, this logic has been disabled.
if (navigator.onLine && this.localDatabase.needScanning) {
this.localDatabase.needScanning = false;
await this.plugin.$$performFullScan();
await this.core.$$performFullScan();
}
}
async watchWindowVisibilityAsync() {
if (this.settings.suspendFileWatching) return;
if (!this.settings.isConfigured) return;
if (!this.plugin.isReady) return;
if (!this.core.$$isReady()) return;
if (this.isLastHidden && !this.hasFocus) {
// NO OP while non-focused after made hidden;
@@ -124,7 +132,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
await this.core.$everyBeforeSuspendProcess();
} else {
// suspend all temporary.
if (this.plugin.suspended) return;
if (this.core.$$isSuspended()) return;
if (!this.hasFocus) return;
await this.core.$everyOnResumeProcess();
await this.core.$everyAfterResumeProcess();
@@ -133,7 +141,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
watchWorkspaceOpen(file: TFile | null) {
if (this.settings.suspendFileWatching) return;
if (!this.settings.isConfigured) return;
if (!this.plugin.isReady) return;
if (!this.core.$$isReady()) return;
if (!file) return;
scheduleTask("watch-workspace-open", 500, () => fireAndForget(() => this.watchWorkspaceOpenAsync(file)));
}
@@ -141,12 +149,12 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
async watchWorkspaceOpenAsync(file: TFile) {
if (this.settings.suspendFileWatching) return;
if (!this.settings.isConfigured) return;
if (!this.plugin.isReady) return;
if (!this.core.$$isReady()) return;
await this.core.$everyCommitPendingFileEvent();
if (file == null) {
return;
}
if (this.settings.syncOnFileOpen && !this.plugin.suspended) {
if (this.settings.syncOnFileOpen && !this.core.$$isSuspended()) {
await this.core.$$replicate();
}
await this.core.$$queueConflictCheckIfOpen(file.path as FilePathWithPrefix);
@@ -158,9 +166,8 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
return Promise.resolve(true);
}
$$askReload(message?: string) {
if (this.core.isReloadingScheduled) {
if (this.core.$$isReloadingScheduled()) {
this._log(`Reloading is already scheduled`, LOG_LEVEL_VERBOSE);
return;
}
@@ -168,16 +175,17 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
const RESTART_NOW = "Yes, restart immediately";
const RESTART_AFTER_STABLE = "Yes, schedule a restart after stabilisation";
const RETRY_LATER = "No, Leave it to me";
const ret = await this.core.confirm.askSelectStringDialogue(message || "Do you want to restart and reload Obsidian now?", [
RESTART_AFTER_STABLE,
RESTART_NOW,
RETRY_LATER], { defaultAction: RETRY_LATER });
const ret = await this.core.confirm.askSelectStringDialogue(
message || "Do you want to restart and reload Obsidian now?",
[RESTART_AFTER_STABLE, RESTART_NOW, RETRY_LATER],
{ defaultAction: RETRY_LATER }
);
if (ret == RESTART_NOW) {
this._performAppReload();
} else if (ret == RESTART_AFTER_STABLE) {
this.core.$$scheduleAppReload();
}
})
});
}
$$scheduleAppReload() {
if (!this.core._totalProcessingCount) {
@@ -194,24 +202,39 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs
const proc = this.core.processingFileEventCount.value;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const __ = __tick.value;
return dbCount + replicationCount + storageApplyingCount + chunkCount + pluginScanCount + hiddenFilesCount + conflictProcessCount + e + proc;
})
this.plugin.registerInterval(setInterval(() => {
__tick.value++;
}, 1000) as unknown as number);
return (
dbCount +
replicationCount +
storageApplyingCount +
chunkCount +
pluginScanCount +
hiddenFilesCount +
conflictProcessCount +
e +
proc
);
});
this.plugin.registerInterval(
setInterval(() => {
__tick.value++;
}, 1000) as unknown as number
);
let stableCheck = 3;
this.core._totalProcessingCount.onChanged(e => {
this.core._totalProcessingCount.onChanged((e) => {
if (e.value == 0) {
if (stableCheck-- <= 0) {
this._performAppReload();
}
this._log(`Obsidian will be restarted soon! (Within ${stableCheck} seconds)`, LOG_LEVEL_NOTICE, "restart-notice");
this._log(
`Obsidian will be restarted soon! (Within ${stableCheck} seconds)`,
LOG_LEVEL_NOTICE,
"restart-notice"
);
} else {
stableCheck = 3;
}
})
});
}
}
}
}

View File

@@ -4,9 +4,7 @@ import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> {
// UI
addIcon(
"replicate",
@@ -19,22 +17,21 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
);
this.addRibbonIcon("replicate", "Replicate", async () => {
await this.plugin.$$replicate(true);
await this.core.$$replicate(true);
}).addClass("livesync-ribbon-replicate");
this.addCommand({
id: "livesync-replicate",
name: "Replicate now",
callback: async () => {
await this.plugin.$$replicate();
await this.core.$$replicate();
},
});
this.addCommand({
id: "livesync-dump",
name: "Dump information of this doc ",
callback: () => {
const file = this.plugin.$$getActiveFilePath();
const file = this.core.$$getActiveFilePath();
if (!file) return;
fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false));
},
@@ -45,7 +42,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
const file = view.file;
if (!file) return;
void this.plugin.$$queueConflictCheckIfOpen(file.path as FilePathWithPrefix);
void this.core.$$queueConflictCheckIfOpen(file.path as FilePathWithPrefix);
},
});
@@ -60,23 +57,23 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
this.settings.liveSync = true;
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
}
await this.plugin.realizeSettingSyncMode();
await this.plugin.saveSettings();
await this.core.$$realizeSettingSyncMode();
await this.core.$$saveSettingData();
},
});
this.addCommand({
id: "livesync-suspendall",
name: "Toggle All Sync.",
callback: async () => {
if (this.plugin.suspended) {
this.plugin.suspended = false;
if (this.core.$$isSuspended()) {
this.core.$$setSuspended(false);
this._log("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
} else {
this.plugin.suspended = true;
this.core.$$setSuspended(true);
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
}
await this.plugin.realizeSettingSyncMode();
await this.plugin.saveSettings();
await this.core.$$realizeSettingSyncMode();
await this.core.$$saveSettingData();
},
});
@@ -84,9 +81,9 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
id: "livesync-scan-files",
name: "Scan storage and database again",
callback: async () => {
await this.plugin.$$performFullScan(true)
}
})
await this.core.$$performFullScan(true);
},
});
this.addCommand({
id: "livesync-runbatch",
@@ -94,21 +91,20 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
callback: async () => {
await this.core.$everyCommitPendingFileEvent();
},
})
});
// TODO, Replicator is possibly one of features. It should be moved to features.
this.addCommand({
id: "livesync-abortsync",
name: "Abort synchronization immediately",
callback: () => {
this.plugin.replicator.terminateSync();
this.core.replicator.terminateSync();
},
})
});
return Promise.resolve(true);
}
$everyOnload(): Promise<boolean> {
this.app.workspace.onLayoutReady(this.plugin.onLiveSyncReady.bind(this.plugin));
this.app.workspace.onLayoutReady(this.core.$$onLiveSyncReady.bind(this.core));
// eslint-disable-next-line no-unused-labels
return Promise.resolve(true);
}
@@ -124,13 +120,10 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
await leaves[0].setViewState({
type: viewType,
active: true,
})
});
}
if (leaves.length > 0) {
this.app.workspace.revealLeaf(
leaves[0]
);
this.app.workspace.revealLeaf(leaves[0]);
}
}
}
}

View File

@@ -0,0 +1,12 @@
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
export class ModuleExtraSyncObsidian extends AbstractObsidianModule implements IObsidianModule {
deviceAndVaultName: string = "";
$$getDeviceAndVaultName(): string {
return this.deviceAndVaultName;
}
$$setDeviceAndVaultName(name: string): void {
this.deviceAndVaultName = name;
}
}

View File

@@ -8,77 +8,89 @@ import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts";
import { writable } from "svelte/store";
export class ModuleDev extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> {
__onMissingTranslation(() => {});
return Promise.resolve(true);
}
$everyOnloadAfterLoadSettings(): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
__onMissingTranslation(() => { });
// eslint-disable-next-line no-unused-labels
__onMissingTranslation((key) => {
const now = new Date();
const filename = `missing-translation-`
const filename = `missing-translation-`;
const time = now.toISOString().split("T")[0];
const outFile = `${filename}${time}.jsonl`;
const piece = JSON.stringify(
{
[key]: {}
}
)
const piece = JSON.stringify({
[key]: {},
});
const writePiece = piece.substring(1, piece.length - 1) + ",";
fireAndForget(async () => {
try {
await this.core.storageAccess.ensureDir(this.app.vault.configDir + "/ls-debug/");
await this.core.storageAccess.appendHiddenFile(this.app.vault.configDir + "/ls-debug/" + outFile, writePiece + "\n")
await this.core.storageAccess.appendHiddenFile(
this.app.vault.configDir + "/ls-debug/" + outFile,
writePiece + "\n"
);
} catch (ex) {
this._log(`Could not write ${outFile}`, LOG_LEVEL_VERBOSE);
this._log(`Missing translation: ${writePiece}`, LOG_LEVEL_VERBOSE);
this._log(ex, LOG_LEVEL_VERBOSE);
}
});
})
});
type STUB = {
toc: Set<string>,
stub: { [key: string]: { [key: string]: Map<string, Record<string, string>> } }
toc: Set<string>;
stub: { [key: string]: { [key: string]: Map<string, Record<string, string>> } };
};
eventHub.onEvent("document-stub-created", (e: CustomEvent<STUB>) => {
eventHub.onEvent("document-stub-created", (detail: STUB) => {
fireAndForget(async () => {
const stub = e.detail.stub;
const toc = e.detail.toc;
const stub = detail.stub;
const toc = detail.toc;
const stubDocX =
Object.entries(stub).map(([key, value]) => {
return [`## ${key}`, Object.entries(value).
map(([key2, value2]) => {
return [`### ${key2}`,
([...(value2.entries())].map(([key3, value3]) => {
// return `#### ${key3}` + "\n" + JSON.stringify(value3);
const isObsolete = value3["is_obsolete"] ? " (obsolete)" : "";
const desc = value3["desc"] ?? "";
const key = value3["key"] ? "Setting key: " + value3["key"] + "\n" : "";
return `#### ${key3}${isObsolete}\n${key}${desc}\n`
}))].flat()
}).flat()].flat()
}).flat();
const stubDocMD = `
const stubDocX = Object.entries(stub)
.map(([key, value]) => {
return [
`## ${key}`,
Object.entries(value)
.map(([key2, value2]) => {
return [
`### ${key2}`,
[...value2.entries()].map(([key3, value3]) => {
// return `#### ${key3}` + "\n" + JSON.stringify(value3);
const isObsolete = value3["is_obsolete"] ? " (obsolete)" : "";
const desc = value3["desc"] ?? "";
const key = value3["key"] ? "Setting key: " + value3["key"] + "\n" : "";
return `#### ${key3}${isObsolete}\n${key}${desc}\n`;
}),
].flat();
})
.flat(),
].flat();
})
.flat();
const stubDocMD =
`
| Icon | Description |
| :---: | ----------------------------------------------------------------- |
` +
[...toc.values()].map(e => `${e}`).join("\n") + "\n\n" +
[...toc.values()].map((e) => `${e}`).join("\n") +
"\n\n" +
stubDocX.join("\n");
await this.core.storageAccess.writeHiddenFileAuto(this.app.vault.configDir + "/ls-debug/stub-doc.md", stubDocMD);
})
await this.core.storageAccess.writeHiddenFileAuto(
this.app.vault.configDir + "/ls-debug/stub-doc.md",
stubDocMD
);
});
});
enableTestFunction(this.plugin);
this.registerView(
VIEW_TYPE_TEST,
(leaf) => new TestPaneView(leaf, this.plugin, this)
);
this.registerView(VIEW_TYPE_TEST, (leaf) => new TestPaneView(leaf, this.plugin, this));
this.addCommand({
id: "view-test",
name: "Open Test dialogue",
callback: () => {
void this.core.$$showView(VIEW_TYPE_TEST);
}
},
});
return Promise.resolve(true);
}
@@ -108,4 +120,4 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule
// this.addTestResult("Test of test3", true);
return this.testDone();
}
}
}

View File

@@ -4,11 +4,10 @@ import { shareRunningResult } from "octagonal-wheels/concurrency/lock";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule";
export class ModuleIntegratedTest extends AbstractObsidianModule implements IObsidianModule {
async waitFor(proc: () => Promise<boolean>, timeout = 10000): Promise<boolean> {
await delay(100);
const start = Date.now();
while (!await proc()) {
while (!(await proc())) {
if (timeout > 0) {
if (Date.now() - start > timeout) {
this._log(`Timeout`);
@@ -40,25 +39,27 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
}
}
async assert(proc: () => Promise<boolean>): Promise<boolean> {
if (!await proc()) {
if (!(await proc())) {
this._log(`Assertion failed`);
return false;
}
return true;
}
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}`);
}
return true;
}
tryReplicate() {
if (!this.settings.liveSync) {
return shareRunningResult("replicate-test", async () => { await this.core.$$replicate() });
return shareRunningResult("replicate-test", async () => {
await this.core.$$replicate();
});
}
}
async readStorageContent(file: FilePathWithPrefix): Promise<string | undefined> {
if (!await this.core.storageAccess.isExistsIncludeHidden(file)) {
if (!(await this.core.storageAccess.isExistsIncludeHidden(file))) {
return undefined;
}
return await this.core.storageAccess.readHiddenFileText(file);
@@ -70,13 +71,14 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
await this.core.$anyResolveConflictByNewest(stepFile);
await this.core.storageAccess.writeFileAuto(stepFile, stepContent);
await this._orDie(`Wait for acknowledge ${no}`, async () => {
if (!await this.waitWithReplicating(
async () => {
return await this.storageContentIsEqual(stepAckFile, stepContent)
}, 20000)
) return false;
if (
!(await this.waitWithReplicating(async () => {
return await this.storageContentIsEqual(stepAckFile, stepContent);
}, 20000))
)
return false;
return true;
})
});
return true;
}
async _join(no: number, title: string): Promise<boolean> {
@@ -86,14 +88,14 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
const stepContent = `Step ${no}`;
await this._orDie(`Wait for step ${no} (${title})`, async () => {
if (!await this.waitWithReplicating(
async () => {
return await this.storageContentIsEqual(stepFile, stepContent)
}, 20000)
) return false;
if (
!(await this.waitWithReplicating(async () => {
return await this.storageContentIsEqual(stepFile, stepContent);
}, 20000))
)
return false;
return true;
}
)
});
await this.core.$anyResolveConflictByNewest(stepAckFile);
await this.core.storageAccess.writeFileAuto(stepAckFile, stepContent);
await this.tryReplicate();
@@ -105,13 +107,13 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
title,
isGameChanger,
proc,
check
check,
}: {
step: number,
title: string,
isGameChanger: boolean,
proc: () => Promise<any>,
check: () => Promise<boolean>,
step: number;
title: string;
isGameChanger: boolean;
proc: () => Promise<any>;
check: () => Promise<boolean>;
}): Promise<boolean> {
if (isGameChanger) {
await this._proceed(step, title);
@@ -121,9 +123,7 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
this._log(`Error: ${e}`);
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 {
return await this._join(step, title);
}
@@ -135,14 +135,21 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
// async testReceiver(testMain: (testFileName: FilePathWithPrefix) => Promise<boolean>): Promise<boolean> {
// }
async nonLiveTestRunner(isLeader: boolean, testMain: (testFileName: FilePathWithPrefix, isLeader: boolean) => Promise<boolean>): Promise<boolean> {
async nonLiveTestRunner(
isLeader: boolean,
testMain: (testFileName: FilePathWithPrefix, isLeader: boolean) => Promise<boolean>
): Promise<boolean> {
const storage = this.core.storageAccess;
// const database = this.core.databaseFileAccess;
// const _orDie = this._orDie.bind(this);
const testCommandFile = "IT.md" as FilePathWithPrefix;
const textCommandResponseFile = "ITx.md" as FilePathWithPrefix;
let testFileName: FilePathWithPrefix;
this.addTestResult("-------Starting ... ", true, `Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}`);
this.addTestResult(
"-------Starting ... ",
true,
`Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}`
);
if (isLeader) {
await this._proceed(0, "start");
}
@@ -154,14 +161,14 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
isGameChanger: isLeader,
proc: async () => await storage.removeHidden(testCommandFile),
check: async () => !(await storage.isExistsIncludeHidden(testCommandFile)),
})
});
await this.performStep({
step: 1,
title: "Make sure that command File Not Exists On Receiver",
isGameChanger: !isLeader,
proc: async () => await storage.removeHidden(textCommandResponseFile),
check: async () => !(await storage.isExistsIncludeHidden(textCommandResponseFile)),
})
});
await this.performStep({
step: 2,
@@ -173,14 +180,14 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
await storage.writeFileAuto(testCommandFile, testFileName);
},
check: () => Promise.resolve(true),
})
});
await this.performStep({
step: 3,
title: "Wait for the command file to be arrived",
isGameChanger: !isLeader,
proc: async () => { },
proc: async () => {},
check: async () => await storage.isExistsIncludeHidden(testCommandFile),
})
});
await this.performStep({
step: 4,
@@ -190,34 +197,31 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
await storage.writeHiddenFileAuto(textCommandResponseFile, "!");
},
check: () => Promise.resolve(true),
})
});
await this.performStep({
step: 5,
title: "Wait for the response file to be arrived",
isGameChanger: isLeader,
proc: async () => { },
proc: async () => {},
check: async () => await storage.isExistsIncludeHidden(textCommandResponseFile),
})
});
await this.performStep({
step: 6,
title: "Proceed to begin the test",
isGameChanger: isLeader,
proc: async () => {
},
proc: async () => {},
check: () => Promise.resolve(true),
});
await this.performStep({
step: 6,
title: "Begin the test",
isGameChanger: !false,
proc: async () => {
},
proc: async () => {},
check: () => {
return Promise.resolve(true);
},
})
});
// await this.step(0, isLeader, true);
try {
this.addTestResult("** Main------", true, ``);
@@ -234,15 +238,18 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
}
return true;
// Make sure the
// Make sure the
}
async testBasic(filename: FilePathWithPrefix, isLeader: boolean): Promise<boolean> {
const storage = this.core.storageAccess;
const database = this.core.databaseFileAccess;
await this.addTestResult(`---**Starting Basic Test**---`, true, `Test as ${isLeader ? "Leader" : "Receiver"} command file ${filename}`);
await this.addTestResult(
`---**Starting Basic Test**---`,
true,
`Test as ${isLeader ? "Leader" : "Receiver"} command file ${filename}`
);
// if (isLeader) {
// await this._proceed(0);
// }
@@ -252,10 +259,9 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
step: 0,
title: "Make sure that file is not exist",
isGameChanger: !isLeader,
proc: async () => { },
proc: async () => {},
check: async () => !(await storage.isExists(filename)),
})
});
await this.performStep({
step: 1,
@@ -263,47 +269,47 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs
isGameChanger: isLeader,
proc: async () => await storage.writeFileAuto(filename, "Hello World"),
check: async () => await storage.isExists(filename),
})
});
await this.performStep({
step: 2,
title: "Make sure the file is arrived",
isGameChanger: !isLeader,
proc: async () => { },
proc: async () => {},
check: async () => await storage.isExists(filename),
})
});
await this.performStep({
step: 3,
title: "Update to Hello World 2",
isGameChanger: isLeader,
proc: async () => await storage.writeFileAuto(filename, "Hello World 2"),
check: async () => await this.storageContentIsEqual(filename, "Hello World 2"),
})
});
await this.performStep({
step: 4,
title: "Make sure the modified file is arrived",
isGameChanger: !isLeader,
proc: async () => { },
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, "Hello World 2"),
})
});
await this.performStep({
step: 5,
title: "Update to Hello World 3",
isGameChanger: !isLeader,
proc: async () => await storage.writeFileAuto(filename, "Hello World 3"),
check: async () => await this.storageContentIsEqual(filename, "Hello World 3"),
})
});
await this.performStep({
step: 6,
title: "Make sure the modified file is arrived",
isGameChanger: isLeader,
proc: async () => { },
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, "Hello World 3"),
})
});
const multiLineContent = `Line1:A
Line2:B
Line3:C
Line4:D`
Line4:D`;
await this.performStep({
step: 7,
@@ -311,38 +317,35 @@ Line4:D`
isGameChanger: isLeader,
proc: async () => await storage.writeFileAuto(filename, multiLineContent),
check: async () => await this.storageContentIsEqual(filename, multiLineContent),
})
});
await this.performStep({
step: 8,
title: "Make sure the modified file is arrived",
isGameChanger: !isLeader,
proc: async () => { },
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, multiLineContent),
})
});
// While LiveSync, possibly cannot cause the conflict.
if (!this.settings.liveSync) {
// Step 9 Make Conflict But Resolvable
const multiLineContentL = `Line1:A
Line2:B
Line3:C!
Line4:D`
Line4:D`;
const multiLineContentC = `Line1:A
Line2:bbbbb
Line3:C
Line4:D`
Line4:D`;
await this.performStep({
step: 9,
title: "Progress to be conflicted",
isGameChanger: isLeader,
proc: async () => { },
proc: async () => {},
check: () => Promise.resolve(true),
})
});
await storage.writeFileAuto(filename, isLeader ? multiLineContentL : multiLineContentC);
@@ -350,62 +353,62 @@ Line4:D`
step: 10,
title: "Update As Conflicted",
isGameChanger: !isLeader,
proc: async () => { },
proc: async () => {},
check: () => Promise.resolve(true),
})
});
await this.performStep({
step: 10,
title: "Make sure Automatically resolved",
isGameChanger: isLeader,
proc: async () => { },
proc: async () => {},
check: async () => (await database.getConflictedRevs(filename)).length === 0,
})
});
await this.performStep({
step: 11,
title: "Make sure Automatically resolved",
isGameChanger: !isLeader,
proc: async () => { },
proc: async () => {},
check: async () => (await database.getConflictedRevs(filename)).length === 0,
})
});
const sensiblyMergedContent = `Line1:A
Line2:bbbbb
Line3:C!
Line4:D`
Line4:D`;
await this.performStep({
step: 12,
title: "Make sure Sensibly Merged on Leader",
isGameChanger: isLeader,
proc: async () => { },
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent),
})
});
await this.performStep({
step: 13,
title: "Make sure Sensibly Merged on Receiver",
isGameChanger: !isLeader,
proc: async () => { },
proc: async () => {},
check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent),
})
});
}
await this.performStep({
step: 14,
title: "Delete File",
isGameChanger: isLeader,
proc: async () => { await storage.removeHidden(filename) },
check: async () => !await storage.isExists(filename),
})
proc: async () => {
await storage.removeHidden(filename);
},
check: async () => !(await storage.isExists(filename)),
});
await this.performStep({
step: 15,
title: "Make sure File is deleted",
isGameChanger: !isLeader,
proc: async () => { },
check: async () => !await storage.isExists(filename),
})
proc: async () => {},
check: async () => !(await storage.isExists(filename)),
});
this._log(`The Basic Test has been completed`, LOG_LEVEL_NOTICE);
return true;
}
@@ -429,13 +432,12 @@ Line4:D`
this._log(`Starting Test`);
await this.testBasicEvent(isLeader);
if (this.settings.remoteType == REMOTE_MINIO) await this.testBasicLive(isLeader);
} catch (e) {
this._log(e)
this._log(e);
this._log(`Error: ${e}`);
return Promise.resolve(false);
}
return Promise.resolve(true);
}
}
}

View File

@@ -8,9 +8,13 @@ import { parseYaml, requestUrl, stringifyYaml } from "obsidian";
import type { FilePath } from "../../lib/src/common/types.ts";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
declare global {
interface LSEvents {
"debug-sync-status": string[];
}
}
export class ModuleReplicateTest extends AbstractObsidianModule implements IObsidianModule {
testRootPath = "_test/";
testInfoPath = "_testinfo/";
@@ -18,7 +22,6 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
return this.core.$$getVaultName().indexOf("dev") >= 0 && this.core.$$vaultName().indexOf("recv") < 0;
}
get nameByKind() {
if (!this.isLeader) {
return "RECV";
@@ -45,7 +48,6 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
}
}
async dumpList() {
if (this.settings.syncInternalFiles) {
this._log("Write file list (Include Hidden)");
@@ -69,39 +71,38 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
void this._dumpFileList("files.md").finally(() => {
void this.refreshSyncStatus();
});
}
})
},
});
this.addCommand({
id: "dump-file-structure-ih",
name: "Dump Structure (Include Hidden)",
callback: () => {
const d = "files.md";
void this._dumpFileListIncludeHidden(d);
}
})
},
});
this.addCommand({
id: "dump-file-structure-auto",
name: "Dump Structure",
callback: () => {
void this.dumpList();
}
})
},
});
this.addCommand({
id: "dump-file-test",
name: `Perform Test (Dev) ${this.isLeader ? "(Leader)" : "(Recv)"}`,
callback: () => {
void this.performTestManually();
}
})
},
});
this.addCommand({
id: "watch-sync-result",
name: `Watch sync result is matched between devices`,
callback: () => {
this.watchIsSynchronised = !this.watchIsSynchronised;
void this.refreshSyncStatus();
}
})
},
});
this.app.vault.on("modify", async (file) => {
if (file.path.startsWith(this.testInfoPath)) {
await this.refreshSyncStatus();
@@ -111,7 +112,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
return true;
});
}
})
});
this.statusBarSyncStatus = this.plugin.addStatusBarItem();
return Promise.resolve(true);
}
@@ -158,12 +159,11 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
}
}
async _dumpFileList(outFile?: string) {
const files = this.core.storageAccess.getFiles();
const out = [] as any[];
for (const file of files) {
if (!await this.core.$$isTargetFile(file.path)) {
if (!(await this.core.$$isTargetFile(file.path))) {
continue;
}
if (file.path.startsWith(this.testInfoPath)) continue;
@@ -177,8 +177,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
name: file.name,
size: stat.size,
mtime: stat.mtime,
hash: hashStr
}
hash: hashStr,
};
// const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`;
out.push(item);
}
@@ -197,7 +197,9 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
async _dumpFileListIncludeHidden(outFile?: string) {
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "")
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
.split(",")
.filter((e) => e)
.map((e) => new RegExp(e, "i"));
const out = [] as any[];
const files = await this.core.storageAccess.getFilesIncludeHidden("", undefined, ignorePatterns);
console.dir(files);
@@ -216,8 +218,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
name: file.split("/").pop(),
size: stat.size,
mtime: stat.mtime,
hash: hashStr
}
hash: hashStr,
};
// const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`;
out.push(item);
}
@@ -257,27 +259,27 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
"docs/tech_info.md",
"docs/terms.md",
"docs/troubleshooting.md",
'images/1.png',
'images/2.png',
'images/corrupted_data.png',
'images/hatch.png',
'images/lock_pattern1.png',
'images/lock_pattern2.png',
'images/quick_setup_1.png',
'images/quick_setup_2.png',
'images/quick_setup_3.png',
'images/quick_setup_3b.png',
'images/quick_setup_4.png',
'images/quick_setup_5.png',
'images/quick_setup_6.png',
'images/quick_setup_7.png',
'images/quick_setup_8.png',
'images/quick_setup_9_1.png',
'images/quick_setup_9_2.png',
'images/quick_setup_10.png',
'images/remote_db_setting.png',
'images/write_logs_into_the_file.png',
]
"images/1.png",
"images/2.png",
"images/corrupted_data.png",
"images/hatch.png",
"images/lock_pattern1.png",
"images/lock_pattern2.png",
"images/quick_setup_1.png",
"images/quick_setup_2.png",
"images/quick_setup_3.png",
"images/quick_setup_3b.png",
"images/quick_setup_4.png",
"images/quick_setup_5.png",
"images/quick_setup_6.png",
"images/quick_setup_7.png",
"images/quick_setup_8.png",
"images/quick_setup_9_1.png",
"images/quick_setup_9_2.png",
"images/quick_setup_10.png",
"images/remote_db_setting.png",
"images/write_logs_into_the_file.png",
];
for (const file of files) {
const remote = remoteTopDir + file;
const local = this.testRootPath + file;
@@ -297,7 +299,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
async waitFor(proc: () => Promise<boolean>, timeout = 10000): Promise<boolean> {
await delay(100);
const start = Date.now();
while (!await proc()) {
while (!(await proc())) {
if (timeout > 0) {
if (Date.now() - start > timeout) {
this._log(`Timeout`);
@@ -310,7 +312,6 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
}
async testConflictedManually1() {
await this.core.$$replicate();
const commonFile = `Resolve!
@@ -322,18 +323,20 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
await this.core.$$replicate();
await this.core.$$replicate();
if (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", { timeout: 30, defaultOption: "Yes" }) == "no") {
if (
(await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
const fileA = `Resolve to KEEP THIS
Willy Wonka, Willy Wonka, the amazing chocolatier!!`
Willy Wonka, Willy Wonka, the amazing chocolatier!!`;
const fileB = `Resolve to DISCARD THIS
Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`
Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`;
if (this.isLeader) {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileA);
@@ -341,25 +344,37 @@ Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileB);
}
if (await this.core.confirm.askYesNoDialog("Ready to check the result of Manually 1?", { timeout: 30, defaultOption: "Yes" }) == "no") {
if (
(await this.core.confirm.askYesNoDialog("Ready to check the result of Manually 1?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
await this.core.$$replicate();
await this.core.$$replicate();
if (!await this.waitFor(async () => {
await this.core.$$replicate();
return await this.__assertStorageContent(this.testRootPath + "wonka.md" as FilePath, fileA, false, true) == true;
}, 30000)) {
return await this.__assertStorageContent(this.testRootPath + "wonka.md" as FilePath, fileA, false, true);
if (
!(await this.waitFor(async () => {
await this.core.$$replicate();
return (
(await this.__assertStorageContent(
(this.testRootPath + "wonka.md") as FilePath,
fileA,
false,
true
)) == true
);
}, 30000))
) {
return await this.__assertStorageContent((this.testRootPath + "wonka.md") as FilePath, fileA, false, true);
}
return true;
// We have to check the result
}
async testConflictedManually2() {
await this.core.$$replicate();
const commonFile = `Resolve To concatenate
@@ -371,56 +386,82 @@ ABCDEFG`;
await this.core.$$replicate();
await this.core.$$replicate();
if (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", { timeout: 30, defaultOption: "Yes" }) == "no") {
if (
(await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
const fileA = `Resolve to Concatenate
ABCDEFGHIJKLMNOPQRSTYZ`
ABCDEFGHIJKLMNOPQRSTYZ`;
const fileB = `Resolve to Concatenate
AJKLMNOPQRSTUVWXYZ`
AJKLMNOPQRSTUVWXYZ`;
const concatenated = `Resolve to Concatenate
ABCDEFGHIJKLMNOPQRSTUVWXYZ`
ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
if (this.isLeader) {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileA);
} else {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileB);
}
if (await this.core.confirm.askYesNoDialog("Ready to test conflict Manually 2?", { timeout: 30, defaultOption: "Yes" }) == "no") {
if (
(await this.core.confirm.askYesNoDialog("Ready to test conflict Manually 2?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
await this.core.$$replicate();
await this.core.$$replicate();
if (!await this.waitFor(async () => {
await this.core.$$replicate();
return await this.__assertStorageContent(this.testRootPath + "concat.md" as FilePath, concatenated, false, true) == true;
}, 30000)) {
return await this.__assertStorageContent(this.testRootPath + "concat.md" as FilePath, concatenated, false, true);
if (
!(await this.waitFor(async () => {
await this.core.$$replicate();
return (
(await this.__assertStorageContent(
(this.testRootPath + "concat.md") as FilePath,
concatenated,
false,
true
)) == true
);
}, 30000))
) {
return await this.__assertStorageContent(
(this.testRootPath + "concat.md") as FilePath,
concatenated,
false,
true
);
}
return true;
}
async testConflictAutomatic() {
if (this.isLeader) {
const baseDoc = `Tasks!
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
- [ ] Task 4
`
`;
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", baseDoc);
}
await delay(100)
await delay(100);
await this.core.$$replicate();
await this.core.$$replicate();
if (await this.core.confirm.askYesNoDialog("Ready to test conflict?", { timeout: 30, defaultOption: "Yes" }) == "no") {
if (
(await this.core.confirm.askYesNoDialog("Ready to test conflict?", {
timeout: 30,
defaultOption: "Yes",
})) == "no"
) {
return;
}
const mod1Doc = `Tasks!
@@ -428,14 +469,14 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`
- [v] Task 2
- [ ] Task 3
- [ ] Task 4
`
`;
const mod2Doc = `Tasks!
- [ ] Task 1
- [ ] Task 2
- [v] Task 3
- [ ] Task 4
`
`;
if (this.isLeader) {
await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod1Doc);
} else {
@@ -445,7 +486,10 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`
await this.core.$$replicate();
await this.core.$$replicate();
await delay(1000);
if (await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" }) == "no") {
if (
(await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" })) ==
"no"
) {
return;
}
await this.core.$$replicate();
@@ -455,8 +499,8 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`
- [v] Task 2
- [v] Task 3
- [ ] Task 4
`
return this.__assertStorageContent(this.testRootPath + "task.md" as FilePath, mergedDoc, false, true);
`;
return this.__assertStorageContent((this.testRootPath + "task.md") as FilePath, mergedDoc, false, true);
}
async checkConflictResolution() {
@@ -465,24 +509,27 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`
await this.core.rebuilder.resolveAllConflictedFilesByNewerOnes();
await this.core.$$replicate();
await delay(1000);
if (!await this.testConflictAutomatic()) {
if (!(await this.testConflictAutomatic())) {
this._log("Conflict resolution (Auto) failed", LOG_LEVEL_NOTICE);
return false;
}
if (!await this.testConflictedManually1()) {
if (!(await this.testConflictedManually1())) {
this._log("Conflict resolution (Manual1) failed", LOG_LEVEL_NOTICE);
return false;
}
if (!await this.testConflictedManually2()) {
if (!(await this.testConflictedManually2())) {
this._log("Conflict resolution (Manual2) failed", LOG_LEVEL_NOTICE);
return false;
}
return true;
}
async __assertStorageContent(fileName: FilePath, content: string, inverted = false, showResult = false): Promise<boolean | string> {
async __assertStorageContent(
fileName: FilePath,
content: string,
inverted = false,
showResult = false
): Promise<boolean | string> {
try {
const fileContent = await this.core.storageAccess.readHiddenFileText(fileName);
let result = fileContent === content;
@@ -525,4 +572,4 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`
await this._test("Conflict resolution", async () => await this.checkConflictResolution());
return this.testDone();
}
}
}

View File

@@ -41,7 +41,7 @@
isReady = true;
// performTest();
eventHub.once(EVENT_LAYOUT_READY, async () => {
eventHub.onceEvent(EVENT_LAYOUT_READY, async () => {
if (await plugin.storageAccess.isExistsIncludeHidden("_AUTO_TEST.md")) {
new Notice("Auto test file found, running tests...");
fireAndForget(async () => {
@@ -83,7 +83,7 @@
$: resultLines = $results;
let syncStatus = [] as string[];
eventHub.on<string[]>("debug-sync-status", (status) => {
eventHub.onEvent("debug-sync-status", (status) => {
syncStatus = [...status];
});
</script>

View File

@@ -1,19 +1,15 @@
import {
ItemView,
WorkspaceLeaf
} from "obsidian";
import TestPaneComponent from "./TestPane.svelte"
import type ObsidianLiveSyncPlugin from "../../../main.ts"
import { ItemView, WorkspaceLeaf } from "obsidian";
import TestPaneComponent from "./TestPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import type { ModuleDev } from "../ModuleDev.ts";
export const VIEW_TYPE_TEST = "ols-pane-test";
//Log view
export class TestPaneView extends ItemView {
component?: TestPaneComponent;
plugin: ObsidianLiveSyncPlugin;
moduleDev: ModuleDev;
icon = "view-log";
title: string = "Self-hosted LiveSync Test and Results"
title: string = "Self-hosted LiveSync Test and Results";
navigation = true;
getIcon(): string {
@@ -26,7 +22,6 @@ export class TestPaneView extends ItemView {
this.moduleDev = moduleDev;
}
getViewType() {
return VIEW_TYPE_TEST;
}
@@ -41,7 +36,7 @@ export class TestPaneView extends ItemView {
target: this.contentEl,
props: {
plugin: this.plugin,
moduleDev: this.moduleDev
moduleDev: this.moduleDev,
},
});
await Promise.resolve();

View File

@@ -7,40 +7,44 @@ export function enableTestFunction(plugin_: ObsidianLiveSyncPlugin) {
plugin = plugin_;
}
export function addDebugFileLog(message: any, stackLog = false) {
fireAndForget(serialized("debug-log", async () => {
const now = new Date();
const filename = `debug-log`
const time = now.toISOString().split("T")[0];
const outFile = `${filename}${time}.jsonl`;
// const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
const timestamp = now.toLocaleString();
const timestampEpoch = now;
let out = { "timestamp": timestamp, epoch: timestampEpoch, } as Record<string, any>;
if (message instanceof Error) {
// debugger;
// console.dir(message.stack);
out = { ...out, message };
} else if (stackLog) {
if (stackLog) {
const stackE = new Error();
const stack = stackE.stack;
out = { ...out, stack }
fireAndForget(
serialized("debug-log", async () => {
const now = new Date();
const filename = `debug-log`;
const time = now.toISOString().split("T")[0];
const outFile = `${filename}${time}.jsonl`;
// const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
const timestamp = now.toLocaleString();
const timestampEpoch = now;
let out = { timestamp: timestamp, epoch: timestampEpoch } as Record<string, any>;
if (message instanceof Error) {
// debugger;
// console.dir(message.stack);
out = { ...out, message };
} else if (stackLog) {
if (stackLog) {
const stackE = new Error();
const stack = stackE.stack;
out = { ...out, stack };
}
}
}
if (typeof message == "object") {
out = { ...out, ...message, }
} else {
out = {
result: message
if (typeof message == "object") {
out = { ...out, ...message };
} else {
out = {
result: message,
};
}
}
// const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || "");
// const out
try {
await plugin.storageAccess.appendHiddenFile(plugin.app.vault.configDir + "/ls-debug/" + outFile, JSON.stringify(out) + "\n")
} catch {
//NO OP
}
}));
}
// const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || "");
// const out
try {
await plugin.storageAccess.appendHiddenFile(
plugin.app.vault.configDir + "/ls-debug/" + outFile,
JSON.stringify(out) + "\n"
);
} catch {
//NO OP
}
})
);
}

View File

@@ -7,7 +7,7 @@ const measures = new Map<string, MeasureResult>();
function clearResult(name: string) {
measures.set(name, [0, 0]);
}
async function measureEach(name: string, proc: () => (void | Promise<void>)) {
async function measureEach(name: string, proc: () => void | Promise<void>) {
const [times, spent] = measures.get(name) ?? [0, 0];
const start = performance.now();
@@ -15,57 +15,76 @@ async function measureEach(name: string, proc: () => (void | Promise<void>)) {
if (result instanceof Promise) await result;
const end = performance.now();
measures.set(name, [times + 1, spent + (end - start)]);
}
function formatNumber(num: number) {
return num.toLocaleString('en-US', { maximumFractionDigits: 2 });
return num.toLocaleString("en-US", { maximumFractionDigits: 2 });
}
async function measure(name: string, proc: () => (void | Promise<void>), times: number = 10000, duration: number = 1000): Promise<NamedMeasureResult> {
async function measure(
name: string,
proc: () => void | Promise<void>,
times: number = 10000,
duration: number = 1000
): Promise<NamedMeasureResult> {
const from = Date.now();
let last = times;
clearResult(name);
do {
await measureEach(name, proc);
} while (last-- > 0 && (Date.now() - from) < duration)
} while (last-- > 0 && Date.now() - from < duration);
return [name, measures.get(name) as MeasureResult];
}
// eslint-disable-next-line require-await, @typescript-eslint/require-await
async function formatPerfResults(items: NamedMeasureResult[]) {
return `| Name | Runs | Each | Total |\n| --- | --- | --- | --- | \n` + items.map(e => `| ${e[0]} | ${e[1][0]} | ${e[1][0] != 0 ? formatNumber(e[1][1] / e[1][0]) : "-"} | ${formatNumber(e[1][0])} |`).join("\n");
return (
`| Name | Runs | Each | Total |\n| --- | --- | --- | --- | \n` +
items
.map(
(e) =>
`| ${e[0]} | ${e[1][0]} | ${e[1][0] != 0 ? formatNumber(e[1][1] / e[1][0]) : "-"} | ${formatNumber(e[1][0])} |`
)
.join("\n")
);
}
export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
clearResult("trench");
const trench = new Trench(plugin.simpleStore);
const result = [] as NamedMeasureResult[];
result.push(await measure("trench-short-string", async () => {
const p = trench.evacuate("string");
await p();
}));
result.push(
await measure("trench-short-string", async () => {
const p = trench.evacuate("string");
await p();
})
);
{
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/10kb.png");
const uint8Array = new Uint8Array(testBinary);
result.push(await measure("trench-binary-10kb", async () => {
const p = trench.evacuate(uint8Array);
await p();
}));
result.push(
await measure("trench-binary-10kb", async () => {
const p = trench.evacuate(uint8Array);
await p();
})
);
}
{
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/100kb.jpeg");
const uint8Array = new Uint8Array(testBinary);
result.push(await measure("trench-binary-100kb", async () => {
const p = trench.evacuate(uint8Array);
await p();
}));
result.push(
await measure("trench-binary-100kb", async () => {
const p = trench.evacuate(uint8Array);
await p();
})
);
}
{
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/1mb.png");
const uint8Array = new Uint8Array(testBinary);
result.push(await measure("trench-binary-1mb", async () => {
const p = trench.evacuate(uint8Array);
await p();
}));
result.push(
await measure("trench-binary-1mb", async () => {
const p = trench.evacuate(uint8Array);
await p();
})
);
}
return formatPerfResults(result);
}
}

View File

@@ -2,7 +2,14 @@ import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_pat
import { getPathFromTFile, isValidPath } from "../../../common/utils.ts";
import { decodeBinary, escapeStringToHTML, readString } from "../../../lib/src/string_and_binary/convert.ts";
import ObsidianLiveSyncPlugin from "../../../main.ts";
import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../../lib/src/common/types.ts";
import {
type DocumentID,
type FilePathWithPrefix,
type LoadedEntry,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
} from "../../../lib/src/common/types.ts";
import { Logger } from "../../../lib/src/common/logger.ts";
import { isErrorOfMissingDoc } from "../../../lib/src/pouchdb/utils_couchdb.ts";
import { fireAndForget, getDocData, readContent } from "../../../lib/src/common/utils.ts";
@@ -19,7 +26,7 @@ function isComparableText(path: string) {
}
function isComparableTextDecode(path: string) {
const ext = path.split(".").splice(-1)[0].toLowerCase();
return ["json"].includes(ext)
return ["json"].includes(ext);
}
function readDocument(w: LoadedEntry) {
if (w.data.length == 0) return "";
@@ -54,10 +61,16 @@ export class DocumentHistoryModal extends Modal {
currentDeleted = false;
initialRev?: string;
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id?: DocumentID, revision?: string) {
constructor(
app: App,
plugin: ObsidianLiveSyncPlugin,
file: TFile | FilePathWithPrefix,
id?: DocumentID,
revision?: string
) {
super(app);
this.plugin = plugin;
this.file = (file instanceof TFile) ? getPathFromTFile(file) : file;
this.file = file instanceof TFile ? getPathFromTFile(file) : file;
this.id = id;
this.initialRev = revision;
if (!file && id) {
@@ -95,7 +108,7 @@ export class DocumentHistoryModal extends Modal {
async loadRevs(initialRev?: string) {
if (this.revs_info.length == 0) return;
if (initialRev) {
const rIndex = this.revs_info.findIndex(e => e.rev == initialRev);
const rIndex = this.revs_info.findIndex((e) => e.rev == initialRev);
if (rIndex >= 0) {
this.range.value = `${this.revs_info.length - 1 - rIndex}`;
}
@@ -163,8 +176,7 @@ export class DocumentHistoryModal extends Modal {
} else if (isImage(this.file)) {
const src = this.generateBlobURL("base", w1data);
const overlay = this.generateBlobURL("overlay", readDocument(w2) as Uint8Array);
result =
`<div class='ls-imgdiff-wrap'>
result = `<div class='ls-imgdiff-wrap'>
<div class='overlay'>
<img class='img-base' src="${src}">
<img class='img-overlay' src='${overlay}'>
@@ -174,14 +186,12 @@ export class DocumentHistoryModal extends Modal {
}
}
}
}
if (result == undefined) {
if (typeof w1data != "string") {
if (isImage(this.file)) {
const src = this.generateBlobURL("base", w1data);
result =
`<div class='ls-imgdiff-wrap'>
result = `<div class='ls-imgdiff-wrap'>
<div class='overlay'>
<img class='img-base' src="${src}">
</div>
@@ -193,7 +203,8 @@ export class DocumentHistoryModal extends Modal {
}
}
if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file";
this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
this.contentView.innerHTML =
(this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
}
}
@@ -257,9 +268,9 @@ export class DocumentHistoryModal extends Modal {
const leaf = this.plugin.app.workspace.getLeaf(false);
await leaf.openFile(targetFile);
} else {
Logger("The file could not view on the editor", LOG_LEVEL_NOTICE)
Logger("The file could not view on the editor", LOG_LEVEL_NOTICE);
}
}
};
buttons.createEl("button", { text: "Back to this revision" }, (e) => {
e.addClass("mod-cta");
e.addEventListener("click", () => {
@@ -285,9 +296,9 @@ export class DocumentHistoryModal extends Modal {
onClose() {
const { contentEl } = this;
contentEl.empty();
this.BlobURLs.forEach(value => {
this.BlobURLs.forEach((value) => {
console.log(value);
if (value) URL.revokeObjectURL(value);
})
});
}
}

View File

@@ -6,7 +6,6 @@
import { diff_match_patch } from "../../../deps.ts";
import { DocumentHistoryModal } from "../DocumentHistory/DocumentHistoryModal.ts";
import { isPlainText, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts";
import { TFile } from "../../../deps.ts";
import { getPath } from "../../../common/utils.ts";
export let plugin: ObsidianLiveSyncPlugin;
@@ -105,9 +104,9 @@
}
if (rev == docA._rev) {
if (checkStorageDiff) {
const isExist = await plugin.storageAccess.isExists(stripAllPrefixes(getPath(docA)));
const isExist = await plugin.storageAccess.isExistsIncludeHidden(stripAllPrefixes(getPath(docA)));
if (isExist) {
const data = await plugin.storageAccess.readFileAuto(stripAllPrefixes(getPath(docA)));
const data = await plugin.storageAccess.readHiddenFileBinary(stripAllPrefixes(getPath(docA)));
const d = readAsBlob(doc);
const result = await isDocContentSame(data, d);
if (result) {

View File

@@ -1,13 +1,9 @@
import {
ItemView,
WorkspaceLeaf
} from "../../../deps.ts";
import { ItemView, WorkspaceLeaf } from "../../../deps.ts";
import GlobalHistoryComponent from "./GlobalHistory.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
export const VIEW_TYPE_GLOBAL_HISTORY = "global-history";
export class GlobalHistoryView extends ItemView {
component?: GlobalHistoryComponent;
plugin: ObsidianLiveSyncPlugin;
icon = "clock";
@@ -23,7 +19,6 @@ export class GlobalHistoryView extends ItemView {
this.plugin = plugin;
}
getViewType() {
return VIEW_TYPE_GLOBAL_HISTORY;
}

View File

@@ -27,7 +27,7 @@ export class ConflictResolveModal extends Modal {
if (this.pluginPickMode) {
this.title = "Pick a version";
this.remoteName = `Use ${remoteName || "Remote"}`;
this.localName = "Use Local"
this.localName = "Use Local";
}
// Send cancel signal for the previous merge dialogue
// if not there, simply be ignored.
@@ -48,7 +48,7 @@ export class ConflictResolveModal extends Modal {
this.sendResponse(CANCELLED);
}
});
}, 10)
}, 10);
// sendValue("close-resolve-conflict:" + this.filename, false);
this.titleEl.setText(this.title);
contentEl.empty();
@@ -60,28 +60,47 @@ export class ConflictResolveModal extends Modal {
const x1 = v[0];
const x2 = v[1];
if (x1 == DIFF_DELETE) {
diff += "<span class='deleted'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
diff +=
"<span class='deleted'>" +
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
"</span>";
} else if (x1 == DIFF_EQUAL) {
diff += "<span class='normal'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
diff +=
"<span class='normal'>" +
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
"</span>";
} else if (x1 == DIFF_INSERT) {
diff += "<span class='added'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
diff +=
"<span class='added'>" +
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
"</span>";
}
}
diff = diff.replace(/\n/g, "<br>");
div.innerHTML = diff;
const div2 = contentEl.createDiv("");
const date1 = new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
const date2 = new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
const date1 =
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
const date2 =
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
div2.innerHTML = `
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
`;
contentEl.createEl("button", { text: this.localName }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.right.rev))).style.marginRight = "4px";
contentEl.createEl("button", { text: this.remoteName }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.left.rev))).style.marginRight = "4px";
contentEl.createEl("button", { text: this.localName }, (e) =>
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
).style.marginRight = "4px";
contentEl.createEl("button", { text: this.remoteName }, (e) =>
e.addEventListener("click", () => this.sendResponse(this.result.left.rev))
).style.marginRight = "4px";
if (!this.pluginPickMode) {
contentEl.createEl("button", { text: "Concat both" }, (e) => e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT))).style.marginRight = "4px";
contentEl.createEl("button", { text: "Concat both" }, (e) =>
e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT))
).style.marginRight = "4px";
}
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => e.addEventListener("click", () => this.sendResponse(CANCELLED))).style.marginRight = "4px";
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
e.addEventListener("click", () => this.sendResponse(CANCELLED))
).style.marginRight = "4px";
}
sendResponse(result: MergeDialogResult) {
@@ -106,4 +125,4 @@ export class ConflictResolveModal extends Modal {
if (r === RESULT_TIMED_OUT) return CANCELLED;
return r;
}
}
}

View File

@@ -1,13 +1,9 @@
import {
ItemView,
WorkspaceLeaf
} from "obsidian";
import { ItemView, WorkspaceLeaf } from "obsidian";
import LogPaneComponent from "./LogPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
export const VIEW_TYPE_LOG = "log-log";
//Log view
export class LogPaneView extends ItemView {
component?: LogPaneComponent;
plugin: ObsidianLiveSyncPlugin;
icon = "view-log";
@@ -23,7 +19,6 @@ export class LogPaneView extends ItemView {
this.plugin = plugin;
}
getViewType() {
return VIEW_TYPE_LOG;
}
@@ -35,8 +30,7 @@ export class LogPaneView extends ItemView {
async onOpen() {
this.component = new LogPaneComponent({
target: this.contentEl,
props: {
},
props: {},
});
await Promise.resolve();
}

View File

@@ -1,23 +1,17 @@
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { VIEW_TYPE_GLOBAL_HISTORY, GlobalHistoryView } from "./GlobalHistory/GlobalHistoryView.ts";
export class ModuleObsidianGlobalHistory extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> {
this.addCommand({
id: "livesync-global-history",
name: "Show vault history",
callback: () => {
this.showGlobalHistory()
}
})
this.showGlobalHistory();
},
});
this.registerView(
VIEW_TYPE_GLOBAL_HISTORY,
(leaf) => new GlobalHistoryView(leaf, this.plugin)
);
this.registerView(VIEW_TYPE_GLOBAL_HISTORY, (leaf) => new GlobalHistoryView(leaf, this.plugin));
return Promise.resolve(true);
}
@@ -25,5 +19,4 @@ export class ModuleObsidianGlobalHistory extends AbstractObsidianModule implemen
showGlobalHistory() {
void this.core.$$showView(VIEW_TYPE_GLOBAL_HISTORY);
}
}
}

View File

@@ -1,11 +1,20 @@
import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MISSING_OR_ERROR, type DocumentID, type FilePathWithPrefix, type diff_result } from "../../lib/src/common/types.ts";
import {
CANCELLED,
LEAVE_TO_SUBSEQUENT,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
MISSING_OR_ERROR,
type DocumentID,
type FilePathWithPrefix,
type diff_result,
} from "../../lib/src/common/types.ts";
import { ConflictResolveModal } from "./InteractiveConflictResolving/ConflictResolveModal.ts";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { displayRev, getPath, getPathWithoutPrefix } from "../../common/utils.ts";
import { fireAndForget } from "octagonal-wheels/promises";
export class ModuleInteractiveConflictResolver extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> {
this.addCommand({
id: "livesync-conflictcheck",
@@ -13,14 +22,14 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
callback: async () => {
await this.pickFileForResolve();
},
})
});
this.addCommand({
id: "livesync-all-conflictcheck",
name: "Resolve all conflicted files",
callback: async () => {
await this.allConflictCheck();
},
})
});
return Promise.resolve(true);
}
@@ -50,18 +59,26 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
// Create a new file by concatenating both conflicted revisions.
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
const delRev = testDoc._conflicts[0];
if (!await this.core.databaseFileAccess.storeContent(filename, p)) {
if (!(await this.core.databaseFileAccess.storeContent(filename, p))) {
this._log(`Concatenated content cannot be stored:${filename}`, LOG_LEVEL_NOTICE);
return false;
}
// 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage.
if (await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated") == MISSING_OR_ERROR) {
this._log(`Concatenated saved, but cannot delete conflicted revisions: ${filename}, (${displayRev(delRev)})`, LOG_LEVEL_NOTICE);
if (
(await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated")) ==
MISSING_OR_ERROR
) {
this._log(
`Concatenated saved, but cannot delete conflicted revisions: ${filename}, (${displayRev(delRev)})`,
LOG_LEVEL_NOTICE
);
return false;
}
} else if (typeof toDelete === "string") {
// Select one of the conflicted revision to delete.
if (await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected") == MISSING_OR_ERROR) {
if (
(await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected")) == MISSING_OR_ERROR
) {
this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE);
return false;
}
@@ -72,7 +89,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
// In here, some merge has been processed.
// So we have to run replication if configured.
// TODO: Make this is as a event request
if (this.settings.syncAfterMerge && !this.plugin.suspended) {
if (this.settings.syncAfterMerge && !this.core.$$isSuspended()) {
await this.core.$$waitForReplicationOnce();
}
// And, check it again.
@@ -83,22 +100,21 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
while (await this.pickFileForResolve());
}
async pickFileForResolve() {
const notes: { id: DocumentID, path: FilePathWithPrefix, dispPath: string, mtime: number }[] = [];
const notes: { id: DocumentID; path: FilePathWithPrefix; dispPath: string; mtime: number }[] = [];
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (!("_conflicts" in doc)) continue;
notes.push({ id: doc._id, path: getPath(doc), dispPath: getPathWithoutPrefix(doc), mtime: doc.mtime });
}
notes.sort((a, b) => b.mtime - a.mtime);
const notesList = notes.map(e => e.dispPath);
const notesList = notes.map((e) => e.dispPath);
if (notesList.length == 0) {
this._log("There are no conflicted documents", LOG_LEVEL_NOTICE);
return false;
}
const target = await this.plugin.confirm.askSelectString("File to resolve conflict", notesList);
const target = await this.core.confirm.askSelectString("File to resolve conflict", notesList);
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.core.$$waitForAllConflictProcessed();
return true;
@@ -106,44 +122,28 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
return false;
}
// async resolveConflictByNewerEntry(path: FilePathWithPrefix) {
// const id = await this.plugin.$$path2id(path);
// const doc = await this.localDatabase.getRaw<AnyEntry>(id, { conflicts: true });
// // If there is no conflict, return with false.
// if (!("_conflicts" in doc) || doc._conflicts === undefined) return false;
// if (doc._conflicts.length == 0) return false;
// this._log(`Hidden file conflicted:${getPath(doc)}`);
// const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
// const revA = doc._rev;
// const revB = conflicts[0];
// const revBDoc = await this.localDatabase.getRaw<EntryDoc>(id, { rev: revB });
// // determine which revision should been deleted.
// // simply check modified time
// const mtimeA = ("mtime" in doc && doc.mtime) || 0;
// const mtimeB = ("mtime" in revBDoc && revBDoc.mtime) || 0;
// const delRev = mtimeA < mtimeB ? revA : revB;
// // delete older one.
// await this.localDatabase.removeRevision(id, delRev);
// this._log(`Older one has been deleted:${getPath(doc)}`);
// return true;
// }
async $allScanStat(): Promise<boolean> {
const notes: { path: string, mtime: number }[] = [];
const notes: { path: string; mtime: number }[] = [];
this._log(`Checking conflicted files`, LOG_LEVEL_VERBOSE);
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (!("_conflicts" in doc)) continue;
notes.push({ path: getPath(doc), mtime: doc.mtime });
}
if (notes.length > 0) {
this.plugin.confirm.askInPopup(`conflicting-detected-on-safety`, `Some files have been left conflicted! Press {HERE} to resolve them, or you can do it later by "Pick a file to resolve conflict`, (anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
fireAndForget(() => this.allConflictCheck())
});
}
this.core.confirm.askInPopup(
`conflicting-detected-on-safety`,
`Some files have been left conflicted! Press {HERE} to resolve them, or you can do it later by "Pick a file to resolve conflict`,
(anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
fireAndForget(() => this.allConflictCheck());
});
}
);
this._log(
`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`,
LOG_LEVEL_VERBOSE
);
this._log(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL_VERBOSE);
for (const note of notes) {
this._log(`Conflicted: ${note.path}`);
}
@@ -152,5 +152,4 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
}
return true;
}
}
}

View File

@@ -1,8 +1,23 @@
import { computed, reactive, reactiveSource, type ReactiveValue } from "octagonal-wheels/dataobject/reactive";
import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type LOG_LEVEL } from "../../lib/src/common/types.ts";
import {
LOG_LEVEL_DEBUG,
LOG_LEVEL_INFO,
LOG_LEVEL_VERBOSE,
PREFIXMD_LOGFILE,
type DatabaseConnectingStatus,
type LOG_LEVEL,
} from "../../lib/src/common/types.ts";
import { cancelTask, scheduleTask } from "octagonal-wheels/concurrency/task";
import { fireAndForget, isDirty, throttle } from "../../lib/src/common/utils.ts";
import { collectingChunks, pluginScanningCount, hiddenFilesEventCount, hiddenFilesProcessingCount, type LogEntry, logStore, logMessages } from "../../lib/src/mock_and_interop/stores.ts";
import {
collectingChunks,
pluginScanningCount,
hiddenFilesEventCount,
hiddenFilesProcessingCount,
type LogEntry,
logStore,
logMessages,
} from "../../lib/src/mock_and_interop/stores.ts";
import { eventHub } from "../../lib/src/hub/hub.ts";
import { EVENT_FILE_RENAMED, EVENT_LAYOUT_READY, EVENT_LEAF_ACTIVE_CHANGED } from "../../common/events.ts";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
@@ -22,15 +37,16 @@ setGlobalLogFunction((message: any, level?: number, key?: string) => {
let recentLogs = [] as string[];
// Recent log splicer
const recentLogProcessor = new QueueProcessor((logs: string[]) => {
recentLogs = [...recentLogs, ...logs].splice(-200);
logMessages.value = recentLogs;
}, { batchSize: 25, delay: 10, suspended: false, concurrentLimit: 1 }).resumePipeLine();
const recentLogProcessor = new QueueProcessor(
(logs: string[]) => {
recentLogs = [...recentLogs, ...logs].splice(-200);
logMessages.value = recentLogs;
},
{ batchSize: 25, delay: 10, suspended: false, concurrentLimit: 1 }
).resumePipeLine();
// logStore.intercept(e => e.slice(Math.min(e.length - 200, 0)));
export class ModuleLog extends AbstractObsidianModule implements IObsidianModule {
registerView = this.plugin.registerView.bind(this.plugin);
statusBar?: HTMLElement;
@@ -41,7 +57,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
logHistory?: HTMLDivElement;
messageArea?: HTMLDivElement;
statusBarLabels!: ReactiveValue<{ message: string, status: string }>;
statusBarLabels!: ReactiveValue<{ message: string; status: string }>;
statusLog = reactiveSource("");
notifies: { [key: string]: { notice: Notice; count: number } } = {};
@@ -52,7 +68,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
const formatted = reactiveSource("");
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
let maxLen = 1;
numI.onChanged(numX => {
numI.onChanged((numX) => {
const num = numX.value;
const numLen = `${Math.abs(num)}`.length + 1;
maxLen = maxLen < numLen ? numLen : maxLen;
@@ -63,30 +79,30 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
maxLen = 1;
}, 3000);
}
formatted.value = ` ${mark}${`${padSpaces}${num}`.slice(-(maxLen))}`;
})
formatted.value = ` ${mark}${`${padSpaces}${num}`.slice(-maxLen)}`;
});
return computed(() => formatted.value);
}
const labelReplication = padLeftSpComputed(this.plugin.replicationResultCount, `📥`);
const labelDBCount = padLeftSpComputed(this.plugin.databaseQueueCount, `📄`);
const labelStorageCount = padLeftSpComputed(this.plugin.storageApplyingCount, `💾`);
const labelReplication = padLeftSpComputed(this.core.replicationResultCount, `📥`);
const labelDBCount = padLeftSpComputed(this.core.databaseQueueCount, `📄`);
const labelStorageCount = padLeftSpComputed(this.core.storageApplyingCount, `💾`);
const labelChunkCount = padLeftSpComputed(collectingChunks, `🧩`);
const labelPluginScanCount = padLeftSpComputed(pluginScanningCount, `🔌`);
const labelConflictProcessCount = padLeftSpComputed(this.plugin.conflictProcessQueueCount, `🔩`);
const labelConflictProcessCount = padLeftSpComputed(this.core.conflictProcessQueueCount, `🔩`);
const hiddenFilesCount = reactive(() => hiddenFilesEventCount.value + hiddenFilesProcessingCount.value);
const labelHiddenFilesCount = padLeftSpComputed(hiddenFilesCount, `⚙️`)
const labelHiddenFilesCount = padLeftSpComputed(hiddenFilesCount, `⚙️`);
const queueCountLabelX = reactive(() => {
return `${labelReplication()}${labelDBCount()}${labelStorageCount()}${labelChunkCount()}${labelPluginScanCount()}${labelHiddenFilesCount()}${labelConflictProcessCount()}`;
})
});
const queueCountLabel = () => queueCountLabelX.value;
const requestingStatLabel = computed(() => {
const diff = this.plugin.requestCount.value - this.plugin.responseCount.value;
const diff = this.core.requestCount.value - this.core.responseCount.value;
return diff != 0 ? "📲 " : "";
})
});
const replicationStatLabel = computed(() => {
const e = this.plugin.replicationStat.value;
const e = this.core.replicationStat.value;
const sent = e.sent;
const arrived = e.arrived;
const maxPullSeq = e.maxPullSeq;
@@ -97,10 +113,10 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
let pullLast = "";
let w = "";
const labels: Partial<Record<DatabaseConnectingStatus, string>> = {
"CONNECTED": "⚡",
"JOURNAL_SEND": "📦↑",
"JOURNAL_RECEIVE": "📦↓",
}
CONNECTED: "⚡",
JOURNAL_SEND: "📦↑",
JOURNAL_RECEIVE: "📦↓",
};
switch (e.syncStatus) {
case "CLOSED":
case "COMPLETED":
@@ -117,8 +133,18 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
case "JOURNAL_SEND":
case "JOURNAL_RECEIVE":
w = labels[e.syncStatus] || "⚡";
pushLast = ((lastSyncPushSeq == 0) ? "" : (lastSyncPushSeq >= maxPushSeq ? " (LIVE)" : ` (${maxPushSeq - lastSyncPushSeq})`));
pullLast = ((lastSyncPullSeq == 0) ? "" : (lastSyncPullSeq >= maxPullSeq ? " (LIVE)" : ` (${maxPullSeq - lastSyncPullSeq})`));
pushLast =
lastSyncPushSeq == 0
? ""
: lastSyncPushSeq >= maxPushSeq
? " (LIVE)"
: ` (${maxPushSeq - lastSyncPushSeq})`;
pullLast =
lastSyncPullSeq == 0
? ""
: lastSyncPullSeq >= maxPullSeq
? " (LIVE)"
: ` (${maxPullSeq - lastSyncPullSeq})`;
break;
case "ERRORED":
w = "⚠";
@@ -127,13 +153,13 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
w = "?";
}
return { w, sent, pushLast, arrived, pullLast };
})
const labelProc = padLeftSpComputed(this.plugin.processing, ``);
const labelPend = padLeftSpComputed(this.plugin.totalQueued, `🛫`);
const labelInBatchDelay = padLeftSpComputed(this.plugin.batched, `📬`);
});
const labelProc = padLeftSpComputed(this.core.processing, ``);
const labelPend = padLeftSpComputed(this.core.totalQueued, `🛫`);
const labelInBatchDelay = padLeftSpComputed(this.core.batched, `📬`);
const waitingLabel = computed(() => {
return `${labelProc()}${labelPend()}${labelInBatchDelay()}`;
})
});
const statusLineLabel = computed(() => {
const { w, sent, pushLast, arrived, pullLast } = replicationStatLabel();
const queued = queueCountLabel();
@@ -142,24 +168,26 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
return {
message: `${networkActivity}Sync: ${w}${sent}${pushLast}${arrived}${pullLast}${waiting}${queued}`,
};
})
});
const statusBarLabels = reactive(() => {
const scheduleMessage = this.plugin.isReloadingScheduled ? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n` : "";
const scheduleMessage = this.core.$$isReloadingScheduled()
? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n`
: "";
const { message } = statusLineLabel();
const status = scheduleMessage + this.statusLog.value;
return {
message, status
}
})
message,
status,
};
});
this.statusBarLabels = statusBarLabels;
const applyToDisplay = throttle((label: typeof statusBarLabels.value) => {
// const v = label;
this.applyStatusBarText();
}, 20);
statusBarLabels.onChanged(label => applyToDisplay(label.value))
statusBarLabels.onChanged((label) => applyToDisplay(label.value));
}
$everyOnload(): Promise<boolean> {
@@ -181,11 +209,14 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
const thisFile = this.app.workspace.getActiveFile();
if (!thisFile) return "";
// Case Sensitivity
if (this.core.shouldCheckCaseInsensitive) {
const f = this.core.storageAccess.getFiles().map(e => e.path).filter(e => e.toLowerCase() == thisFile.path.toLowerCase());
if (this.core.$$shouldCheckCaseInsensitive()) {
const f = this.core.storageAccess
.getFiles()
.map((e) => e.path)
.filter((e) => e.toLowerCase() == thisFile.path.toLowerCase());
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.core.$$isTargetFile(thisFile.path))) return "Not synchronised: not a target file";
if (this.core.$$isFileSizeExceeded(thisFile.stat.size)) return "Not synchronised: File size exceeded";
return "";
}
@@ -197,11 +228,10 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
this.adjustStatusDivPosition();
await this.setFileStatus();
});
}
nextFrameQueue: ReturnType<typeof requestAnimationFrame> | undefined = undefined;
logLines: { ttl: number, message: string }[] = [];
logLines: { ttl: number; message: string }[] = [];
applyStatusBarText() {
if (this.nextFrameQueue) {
@@ -222,10 +252,13 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
if (this.settings?.showStatusOnEditor && this.statusDiv) {
if (this.settings.showLongerLogInsideEditor) {
const now = new Date().getTime();
this.logLines = this.logLines.filter(e => e.ttl > now);
const minimumNext = this.logLines.reduce((a, b) => a < b.ttl ? a : b.ttl, Number.MAX_SAFE_INTEGER);
this.logLines = this.logLines.filter((e) => e.ttl > now);
const minimumNext = this.logLines.reduce(
(a, b) => (a < b.ttl ? a : b.ttl),
Number.MAX_SAFE_INTEGER
);
if (this.logLines.length > 0) setTimeout(() => this.applyStatusBarText(), minimumNext - now);
const recent = this.logLines.map(e => e.message);
const recent = this.logLines.map((e) => e.message);
const recentLogs = recent.reverse().join("\n");
if (isDirty("recentLogs", recentLogs)) this.logHistory!.innerText = recentLogs;
}
@@ -237,14 +270,16 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
}
});
scheduleTask("log-hide", 3000, () => { this.statusLog.value = "" });
scheduleTask("log-hide", 3000, () => {
this.statusLog.value = "";
});
}
$allStartOnUnload(): Promise<boolean> {
if (this.statusDiv) {
this.statusDiv.remove();
}
document.querySelectorAll(`.livesync-status`)?.forEach(e => e.remove());
document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove());
return Promise.resolve(true);
}
$everyOnloadStart(): Promise<boolean> {
@@ -264,22 +299,28 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
name: "Show log",
callback: () => {
void this.core.$$showView(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);
}
$everyOnloadAfterLoadSettings(): Promise<boolean> {
logStore.pipeTo(new QueueProcessor(logs => logs.forEach(e => this.core.$$addLog(e.message, e.level, e.key)), { suspended: false, batchSize: 20, concurrentLimit: 1, delay: 0 })).startPipeline();
eventHub.onEvent(EVENT_FILE_RENAMED, (evt: CustomEvent<{ oldPath: string, newPath: string }>) => {
logStore
.pipeTo(
new QueueProcessor((logs) => logs.forEach((e) => this.core.$$addLog(e.message, e.level, e.key)), {
suspended: false,
batchSize: 20,
concurrentLimit: 1,
delay: 0,
})
)
.startPipeline();
eventHub.onEvent(EVENT_FILE_RENAMED, (data) => {
void this.setFileStatus();
});
const w = document.querySelectorAll(`.livesync-status`);
w.forEach(e => e.remove());
w.forEach((e) => e.remove());
this.observeForLogs();
@@ -290,7 +331,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
this.logHistory = this.statusDiv.createDiv({ cls: "livesync-status-loghistory" });
eventHub.onEvent(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition());
if (this.settings?.showStatusOnStatusbar) {
this.statusBar = this.plugin.addStatusBarItem();
this.statusBar = this.core.addStatusBarItem();
this.statusBar.addClass("syncstatusbar");
}
this.adjustStatusDivPosition();
@@ -298,15 +339,20 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
}
writeLogToTheFile(now: Date, vaultName: string, newMessage: string) {
fireAndForget(() => serialized("writeLog", async () => {
const time = now.toISOString().split("T")[0];
const logDate = `${PREFIXMD_LOGFILE}${time}.md`;
const file = await this.core.storageAccess.isExists(normalizePath(logDate));
if (!file) {
await this.core.storageAccess.appendHiddenFile(normalizePath(logDate), "```\n");
}
await this.core.storageAccess.appendHiddenFile(normalizePath(logDate), vaultName + ":" + newMessage + "\n");
}));
fireAndForget(() =>
serialized("writeLog", async () => {
const time = now.toISOString().split("T")[0];
const logDate = `${PREFIXMD_LOGFILE}${time}.md`;
const file = await this.core.storageAccess.isExists(normalizePath(logDate));
if (!file) {
await this.core.storageAccess.appendHiddenFile(normalizePath(logDate), "```\n");
}
await this.core.storageAccess.appendHiddenFile(
normalizePath(logDate),
vaultName + ":" + newMessage + "\n"
);
})
);
}
$$addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void {
if (level == LOG_LEVEL_DEBUG) {
@@ -318,10 +364,15 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL_VERBOSE) {
return;
}
const vaultName = this.plugin.$$getVaultName();
const vaultName = this.core.$$getVaultName();
const now = new Date();
const timestamp = now.toLocaleString();
const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
const messageContent =
typeof message == "string"
? message
: message instanceof Error
? `${message.name}:${message.message}`
: JSON.stringify(message, null, 2);
if (message instanceof Error) {
// debugger;
console.dir(message.stack);
@@ -342,7 +393,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
if (!key) key = messageContent;
if (key in this.notifies) {
// @ts-ignore
const isShown = this.notifies[key].notice.noticeEl?.isShown()
const isShown = this.notifies[key].notice.noticeEl?.isShown();
if (!isShown) {
this.notifies[key].notice = new Notice(messageContent, 0);
}
@@ -369,9 +420,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
} catch {
// NO OP
}
})
});
}
}
}

View File

@@ -1,25 +1,21 @@
import { type TFile } from "obsidian";
import { eventHub, EVENT_REQUEST_SHOW_HISTORY } from "../../common/events.ts";
import { eventHub } from "../../common/events.ts";
import { EVENT_REQUEST_SHOW_HISTORY } from "../../common/obsidianEvents.ts";
import type { FilePathWithPrefix, LoadedEntry, DocumentID } from "../../lib/src/common/types.ts";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { DocumentHistoryModal } from "./DocumentHistory/DocumentHistoryModal.ts";
import { getPath } from "../../common/utils.ts";
import { fireAndForget } from "octagonal-wheels/promises";
export class ModuleObsidianDocumentHistory extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> {
this.addCommand({
id: "livesync-history",
name: "Show history",
callback: () => {
const file = this.core.$$getActiveFilePath();
if (file) this.showHistory(file, undefined);
}
},
});
this.addCommand({
@@ -29,9 +25,12 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule implem
fireAndForget(async () => await this.fileHistory());
},
});
eventHub.on(EVENT_REQUEST_SHOW_HISTORY, ({ file, fileOnDB }: { file: TFile, fileOnDB: LoadedEntry }) => {
this.showHistory(file, fileOnDB._id);
})
eventHub.onEvent(
EVENT_REQUEST_SHOW_HISTORY,
({ file, fileOnDB }: { file: TFile | FilePathWithPrefix; fileOnDB: LoadedEntry }) => {
this.showHistory(file, fileOnDB._id);
}
);
return Promise.resolve(true);
}
@@ -40,17 +39,16 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule implem
}
async fileHistory() {
const notes: { id: DocumentID, path: FilePathWithPrefix, dispPath: string, mtime: number }[] = [];
const notes: { id: DocumentID; path: FilePathWithPrefix; dispPath: string; mtime: number }[] = [];
for await (const doc of this.localDatabase.findAllDocs()) {
notes.push({ id: doc._id, path: getPath(doc), dispPath: getPath(doc), mtime: doc.mtime });
}
notes.sort((a, b) => b.mtime - a.mtime);
const notesList = notes.map(e => e.dispPath);
const target = await this.plugin.confirm.askSelectString("File to view History", notesList);
const notesList = notes.map((e) => e.dispPath);
const target = await this.core.confirm.askSelectString("File to view History", notesList);
if (target) {
const targetId = notes.find(e => e.dispPath == target)!;
const targetId = notes.find((e) => e.dispPath == target)!;
this.showHistory(targetId.path, targetId.id);
}
}
}
}

View File

@@ -1,27 +1,33 @@
import { type IObsidianModule, AbstractObsidianModule } from "../AbstractObsidianModule.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
import { type BucketSyncSetting, type ConfigPassphraseStore, type CouchDBConnection, DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, SALT_OF_PASSPHRASE } from "../../lib/src/common/types";
import {
type BucketSyncSetting,
type ConfigPassphraseStore,
type CouchDBConnection,
DEFAULT_SETTINGS,
type ObsidianLiveSyncSettings,
SALT_OF_PASSPHRASE,
} from "../../lib/src/common/types";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT } from "octagonal-wheels/common/logger";
import { encrypt, tryDecrypt } from "octagonal-wheels/encryption";
import { setLang } from "../../lib/src/common/i18n";
import { isCloudantURI } from "../../lib/src/pouchdb/utils_couchdb";
export class ModuleObsidianSettings extends AbstractObsidianModule implements IObsidianModule {
getPassphrase(settings: ObsidianLiveSyncSettings) {
const methods: Record<ConfigPassphraseStore, (() => Promise<string | false>)> = {
const methods: Record<ConfigPassphraseStore, () => Promise<string | false>> = {
"": () => Promise.resolve("*"),
"LOCALSTORAGE": () => Promise.resolve(localStorage.getItem("ls-setting-passphrase") ?? false),
"ASK_AT_LAUNCH": () => this.plugin.confirm.askString("Passphrase", "passphrase", "")
}
LOCALSTORAGE: () => Promise.resolve(localStorage.getItem("ls-setting-passphrase") ?? false),
ASK_AT_LAUNCH: () => this.core.confirm.askString("Passphrase", "passphrase", ""),
};
const method = settings.configPassphraseStore;
const methodFunc = method in methods ? methods[method] : methods[""];
return methodFunc();
}
$$saveDeviceAndVaultName(): void {
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.plugin.$$getVaultName();
localStorage.setItem(lsKey, this.plugin.deviceAndVaultName || "");
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.core.$$getVaultName();
localStorage.setItem(lsKey, this.core.$$getDeviceAndVaultName() || "");
}
usedPassphrase = "";
@@ -29,7 +35,6 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
this.usedPassphrase = "";
}
async decryptConfigurationItem(encrypted: string, passphrase: string) {
const dec = await tryDecrypt(encrypted, passphrase + SALT_OF_PASSPHRASE, false);
if (dec) {
@@ -39,7 +44,6 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
return false;
}
async encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings) {
if (this.usedPassphrase != "") {
return await encrypt(src, this.usedPassphrase + SALT_OF_PASSPHRASE, false);
@@ -47,7 +51,10 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
const passphrase = await this.getPassphrase(settings);
if (passphrase === false) {
this._log("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL_URGENT);
this._log(
"Could not determine passphrase to save data.json! You probably make the configuration sure again!",
LOG_LEVEL_URGENT
);
return "";
}
const dec = await encrypt(src, passphrase + SALT_OF_PASSPHRASE, false);
@@ -60,17 +67,25 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
}
get appId() {
return `${("appId" in this.app ? this.app.appId : "")}`;
return `${"appId" in this.app ? this.app.appId : ""}`;
}
async $$saveSettingData() {
this.plugin.$$saveDeviceAndVaultName();
this.core.$$saveDeviceAndVaultName();
const settings = { ...this.settings };
settings.deviceAndVaultName = "";
if (this.usedPassphrase == "" && !await this.getPassphrase(settings)) {
this._log("Could not determine passphrase for saving data.json! Our data.json have insecure items!", LOG_LEVEL_NOTICE);
if (this.usedPassphrase == "" && !(await this.getPassphrase(settings))) {
this._log(
"Could not determine passphrase for saving data.json! Our data.json have insecure items!",
LOG_LEVEL_NOTICE
);
} else {
if (settings.couchDB_PASSWORD != "" || settings.couchDB_URI != "" || settings.couchDB_USER != "" || settings.couchDB_DBNAME) {
if (
settings.couchDB_PASSWORD != "" ||
settings.couchDB_URI != "" ||
settings.couchDB_USER != "" ||
settings.couchDB_DBNAME
) {
const connectionSetting: CouchDBConnection & BucketSyncSetting = {
couchDB_DBNAME: settings.couchDB_DBNAME,
couchDB_PASSWORD: settings.couchDB_PASSWORD,
@@ -81,9 +96,12 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
endpoint: settings.endpoint,
region: settings.region,
secretKey: settings.secretKey,
useCustomRequestHandler: settings.useCustomRequestHandler
useCustomRequestHandler: settings.useCustomRequestHandler,
};
settings.encryptedCouchDBConnection = await this.encryptConfigurationItem(JSON.stringify(connectionSetting), settings);
settings.encryptedCouchDBConnection = await this.encryptConfigurationItem(
JSON.stringify(connectionSetting),
settings
);
settings.couchDB_PASSWORD = "";
settings.couchDB_DBNAME = "";
settings.couchDB_URI = "";
@@ -98,7 +116,6 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
settings.encryptedPassphrase = await this.encryptConfigurationItem(settings.passphrase, settings);
settings.passphrase = "";
}
}
await this.core.saveData(settings);
eventHub.emitEvent(EVENT_SETTING_SAVED, settings);
@@ -127,7 +144,10 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
}
const passphrase = await this.getPassphrase(settings);
if (passphrase === false) {
this._log("Could not determine passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT);
this._log(
"Could not determine passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!",
LOG_LEVEL_URGENT
);
} else {
if (settings.encryptedCouchDBConnection) {
const keys = [
@@ -139,17 +159,23 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
"bucket",
"endpoint",
"region",
"secretKey"] as (keyof CouchDBConnection | keyof BucketSyncSetting)[];
const decrypted = this.tryDecodeJson(await this.decryptConfigurationItem(settings.encryptedCouchDBConnection, passphrase)) as (CouchDBConnection & BucketSyncSetting);
"secretKey",
] as (keyof CouchDBConnection | keyof BucketSyncSetting)[];
const decrypted = this.tryDecodeJson(
await this.decryptConfigurationItem(settings.encryptedCouchDBConnection, passphrase)
) as CouchDBConnection & BucketSyncSetting;
if (decrypted) {
for (const key of keys) {
if (key in decrypted) {
//@ts-ignore
settings[key] = decrypted[key]
settings[key] = decrypted[key];
}
}
} else {
this._log("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT);
this._log(
"Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!",
LOG_LEVEL_URGENT
);
for (const key of keys) {
//@ts-ignore
settings[key] = "";
@@ -162,11 +188,13 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
if (decrypted) {
settings.passphrase = decrypted;
} else {
this._log("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT);
this._log(
"Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!",
LOG_LEVEL_URGENT
);
settings.passphrase = "";
}
}
}
this.settings = settings;
setLang(this.settings.displayLanguage);
@@ -182,20 +210,23 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
// So, use history is always enabled.
this.settings.useHistory = true;
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.plugin.$$getVaultName();
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.core.$$getVaultName();
if (this.settings.deviceAndVaultName != "") {
if (!localStorage.getItem(lsKey)) {
this.core.deviceAndVaultName = this.settings.deviceAndVaultName;
localStorage.setItem(lsKey, this.core.deviceAndVaultName);
this.core.$$setDeviceAndVaultName(this.settings.deviceAndVaultName);
this.$$saveDeviceAndVaultName();
this.settings.deviceAndVaultName = "";
}
}
if (isCloudantURI(this.settings.couchDB_URI) && this.settings.customChunkSize != 0) {
this._log("Configuration verification founds problems with your configuration. This has been fixed automatically. But you may already have data that cannot be synchronised. If this is the case, please rebuild everything.", LOG_LEVEL_NOTICE)
this._log(
"Configuration verification founds problems with your configuration. This has been fixed automatically. But you may already have data that cannot be synchronised. If this is the case, please rebuild everything.",
LOG_LEVEL_NOTICE
);
this.settings.customChunkSize = 0;
}
this.core.deviceAndVaultName = localStorage.getItem(lsKey) || "";
if (this.core.deviceAndVaultName == "") {
this.core.$$setDeviceAndVaultName(localStorage.getItem(lsKey) || "");
if (this.core.$$getDeviceAndVaultName() == "") {
if (this.settings.usePluginSync) {
this._log("Device name is not set. Plug-in sync has been disabled.", LOG_LEVEL_NOTICE);
this.settings.usePluginSync = false;
@@ -204,4 +235,4 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
// this.core.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
}
}
}

View File

@@ -9,7 +9,6 @@ import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE }
const SETTING_HEADER = "````yaml:livesync-setting\n";
const SETTING_FOOTER = "\n````";
export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> {
this.addCommand({
id: "livesync-export-config",
@@ -19,10 +18,10 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
return this.settings.settingSyncFile != "";
}
fireAndForget(async () => {
await this.plugin.$$saveSettingData();
await this.core.$$saveSettingData();
});
}
})
},
});
this.addCommand({
id: "livesync-import-config",
name: "Parse setting file",
@@ -33,33 +32,33 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
return ret.body != "";
}
if (ctx.file) {
const file = ctx.file
const file = ctx.file;
fireAndForget(async () => await this.checkAndApplySettingFromMarkdown(file.path, false));
}
},
})
eventHub.on("event-file-changed", (info: {
file: FilePathWithPrefix, automated: boolean
}) => {
});
eventHub.onEvent("event-file-changed", (info: { file: FilePathWithPrefix; automated: boolean }) => {
fireAndForget(() => this.checkAndApplySettingFromMarkdown(info.file, info.automated));
});
eventHub.onEvent(EVENT_SETTING_SAVED, (evt: CustomEvent<ObsidianLiveSyncSettings>) => {
const settings = evt.detail;
eventHub.onEvent(EVENT_SETTING_SAVED, (settings: ObsidianLiveSyncSettings) => {
if (settings.settingSyncFile != "") {
fireAndForget(() => this.saveSettingToMarkdown(settings.settingSyncFile));
}
})
});
return Promise.resolve(true);
}
extractSettingFromWholeText(data: string): {
preamble: string, body: string, postscript: string
preamble: string;
body: string;
postscript: string;
} {
if (data.indexOf(SETTING_HEADER) === -1) {
return {
preamble: data, body: "", postscript: ""
}
preamble: data,
body: "",
postscript: "",
};
}
const startMarkerPos = data.indexOf(SETTING_HEADER);
const dataStartPos = startMarkerPos == -1 ? data.length : startMarkerPos;
@@ -69,27 +68,33 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
const ret = {
preamble: data.substring(0, dataStartPos),
body,
postscript: data.substring(dataEndPos + SETTING_FOOTER.length + 1)
}
postscript: data.substring(dataEndPos + SETTING_FOOTER.length + 1),
};
return ret;
}
async parseSettingFromMarkdown(filename: string, data?: string) {
const file = await this.core.storageAccess.isExists(filename);
if (!file) return {
preamble: "", body: "", postscript: "",
};
if (!file)
return {
preamble: "",
body: "",
postscript: "",
};
if (data) {
return this.extractSettingFromWholeText(data);
}
const parseData = data ?? await this.core.storageAccess.readFileText(filename);
const parseData = data ?? (await this.core.storageAccess.readFileText(filename));
return this.extractSettingFromWholeText(parseData);
}
async checkAndApplySettingFromMarkdown(filename: string, automated?: boolean) {
if (automated && !this.settings.notifyAllSettingSyncFile) {
if (!this.settings.settingSyncFile || this.settings.settingSyncFile != filename) {
this._log(`Setting file (${filename}) is not matched to the current configuration. skipped.`, LOG_LEVEL_DEBUG);
this._log(
`Setting file (${filename}) is not matched to the current configuration. skipped.`,
LOG_LEVEL_DEBUG
);
return;
}
}
@@ -104,64 +109,84 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
}
if ("settingSyncFile" in newSetting && newSetting.settingSyncFile != filename) {
this._log("This setting file seems to backed up one. Please fix the filename or settingSyncFile value.", automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE);
this._log(
"This setting file seems to backed up one. Please fix the filename or settingSyncFile value.",
automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE
);
return;
}
let settingToApply = { ...DEFAULT_SETTINGS } as ObsidianLiveSyncSettings;
settingToApply = { ...settingToApply, ...newSetting }
if (!(settingToApply?.writeCredentialsForSettingSync)) {
//New setting does not contains credentials.
settingToApply = { ...settingToApply, ...newSetting };
if (!settingToApply?.writeCredentialsForSettingSync) {
//New setting does not contains credentials.
settingToApply.couchDB_USER = this.settings.couchDB_USER;
settingToApply.couchDB_PASSWORD = this.settings.couchDB_PASSWORD;
settingToApply.passphrase = this.settings.passphrase;
}
const oldSetting = this.generateSettingForMarkdown(this.settings, settingToApply.writeCredentialsForSettingSync);
const oldSetting = this.generateSettingForMarkdown(
this.settings,
settingToApply.writeCredentialsForSettingSync
);
if (!isObjectDifferent(oldSetting, this.generateSettingForMarkdown(settingToApply))) {
this._log("Setting markdown has been detected, but not changed.", automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE);
return
this._log(
"Setting markdown has been detected, but not changed.",
automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE
);
return;
}
const addMsg = this.settings.settingSyncFile != filename ? " (This is not-active file)" : "";
this.plugin.confirm.askInPopup("apply-setting-from-md", `Setting markdown ${filename}${addMsg} has been detected. Apply this from {HERE}.`, (anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
fireAndForget(async () => {
const APPLY_ONLY = "Apply settings";
const APPLY_AND_RESTART = "Apply settings and restart obsidian";
const APPLY_AND_REBUILD = "Apply settings and restart obsidian with red_flag_rebuild.md";
const APPLY_AND_FETCH = "Apply settings and restart obsidian with red_flag_fetch.md";
const CANCEL = "Cancel";
const result = await this.plugin.confirm.askSelectStringDialogue("Ready for apply the setting.", [
APPLY_AND_RESTART,
APPLY_ONLY,
APPLY_AND_FETCH,
APPLY_AND_REBUILD,
CANCEL], { defaultAction: APPLY_AND_RESTART });
if (result == APPLY_ONLY || result == APPLY_AND_RESTART || result == APPLY_AND_REBUILD || result == APPLY_AND_FETCH) {
this.plugin.settings = settingToApply;
await this.plugin.$$saveSettingData();
if (result == APPLY_ONLY) {
this._log("Loaded settings have been applied!", LOG_LEVEL_NOTICE);
return;
this.core.confirm.askInPopup(
"apply-setting-from-md",
`Setting markdown ${filename}${addMsg} has been detected. Apply this from {HERE}.`,
(anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
fireAndForget(async () => {
const APPLY_ONLY = "Apply settings";
const APPLY_AND_RESTART = "Apply settings and restart obsidian";
const APPLY_AND_REBUILD = "Apply settings and restart obsidian with red_flag_rebuild.md";
const APPLY_AND_FETCH = "Apply settings and restart obsidian with red_flag_fetch.md";
const CANCEL = "Cancel";
const result = await this.core.confirm.askSelectStringDialogue(
"Ready for apply the setting.",
[APPLY_AND_RESTART, APPLY_ONLY, APPLY_AND_FETCH, APPLY_AND_REBUILD, CANCEL],
{ defaultAction: APPLY_AND_RESTART }
);
if (
result == APPLY_ONLY ||
result == APPLY_AND_RESTART ||
result == APPLY_AND_REBUILD ||
result == APPLY_AND_FETCH
) {
this.core.settings = settingToApply;
await this.core.$$saveSettingData();
if (result == APPLY_ONLY) {
this._log("Loaded settings have been applied!", LOG_LEVEL_NOTICE);
return;
}
if (result == APPLY_AND_REBUILD) {
await this.core.rebuilder.scheduleRebuild();
}
if (result == APPLY_AND_FETCH) {
await this.core.rebuilder.scheduleFetch();
}
this.core.$$performRestart();
}
if (result == APPLY_AND_REBUILD) {
await this.plugin.rebuilder.scheduleRebuild();
}
if (result == APPLY_AND_FETCH) {
await this.plugin.rebuilder.scheduleFetch();
}
this.plugin.$$performRestart();
}
})
})
})
});
});
}
);
}
generateSettingForMarkdown(settings?: ObsidianLiveSyncSettings, keepCredential?: boolean): Partial<ObsidianLiveSyncSettings> {
generateSettingForMarkdown(
settings?: ObsidianLiveSyncSettings,
keepCredential?: boolean
): Partial<ObsidianLiveSyncSettings> {
const saveData = { ...(settings ? settings : this.settings) } as Partial<ObsidianLiveSyncSettings>;
delete saveData.encryptedCouchDBConnection;
delete saveData.encryptedPassphrase;
delete saveData.additionalSuffixOfDatabaseName;
if (!saveData.writeCredentialsForSettingSync && !keepCredential) {
delete saveData.couchDB_USER;
delete saveData.couchDB_PASSWORD;
@@ -174,7 +199,6 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp
const saveData = this.generateSettingForMarkdown();
const file = await this.core.storageAccess.isExists(filename);
if (!file) {
await this.core.storageAccess.ensureDir(filename);
const initialContent = `This file contains Self-hosted LiveSync settings as YAML.
@@ -188,8 +212,11 @@ We can perform a command in this file.
**Note** Please handle it with all of your care if you have configured to write credentials in.
`
await this.core.storageAccess.writeFileAuto(filename, initialContent + SETTING_HEADER + "\n" + SETTING_FOOTER);
`;
await this.core.storageAccess.writeFileAuto(
filename,
initialContent + SETTING_HEADER + "\n" + SETTING_FOOTER
);
}
// if (!(file instanceof TFile)) {
// this._log(`Markdown Setting: ${filename} already exists as a folder`, LOG_LEVEL_NOTICE);
@@ -203,9 +230,11 @@ We can perform a command in this file.
if (newBody == body) {
this._log("Markdown setting: Nothing had been changed", LOG_LEVEL_VERBOSE);
} else {
await this.core.storageAccess.writeFileAuto(filename, preamble + SETTING_HEADER + newBody + SETTING_FOOTER + postscript);
await this.core.storageAccess.writeFileAuto(
filename,
preamble + SETTING_HEADER + newBody + SETTING_FOOTER + postscript
);
this._log(`Markdown setting: ${filename} has been updated!`, LOG_LEVEL_VERBOSE);
}
}
}
}

View File

@@ -1,16 +1,20 @@
import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSettingTab.ts";
import { type IObsidianModule, AbstractObsidianModule } from "../AbstractObsidianModule.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
import { 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 {
settingTab!: ObsidianLiveSyncSettingTab;
$everyOnloadStart(): Promise<boolean> {
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this.plugin);
this.plugin.addSettingTab(this.settingTab);
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTINGS, () => this.openSetting());
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD, () => {
this.openSetting();
void this.settingTab.enableMinimalSetup();
});
return Promise.resolve(true);
}
@@ -23,8 +27,6 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule implem
}
get appId() {
return `${("appId" in this.app ? this.app.appId : "")}`;
return `${"appId" in this.app ? this.app.appId : ""}`;
}
}
}

View File

@@ -1,4 +1,9 @@
import { type ObsidianLiveSyncSettings, DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types.ts";
import {
type ObsidianLiveSyncSettings,
DEFAULT_SETTINGS,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
} from "../../lib/src/common/types.ts";
import { configURIBase } from "../../common/types.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
import { decrypt, encrypt } from "../../lib/src/encryption/e2ee_v2.ts";
@@ -6,10 +11,12 @@ import { fireAndForget } from "../../lib/src/common/utils.ts";
import { EVENT_REQUEST_COPY_SETUP_URI, EVENT_REQUEST_OPEN_SETUP_URI, eventHub } from "../../common/events.ts";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsidianModule {
$everyOnload(): Promise<boolean> {
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => await this.setupWizard(conf.settings));
this.registerObsidianProtocolHandler(
"setuplivesync",
async (conf: any) => await this.setupWizard(conf.settings)
);
this.addCommand({
id: "livesync-copysetupuri",
@@ -39,30 +46,55 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
}
async command_copySetupURI(stripExtra = true) {
const encryptingPassphrase = await this.core.confirm.askString("Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
if (encryptingPassphrase === false)
return;
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" } as Partial<ObsidianLiveSyncSettings>;
const encryptingPassphrase = await this.core.confirm.askString(
"Encrypt your settings",
"The passphrase to encrypt the setup URI",
"",
true
);
if (encryptingPassphrase === false) return;
const setting = {
...this.settings,
configPassphraseStore: "",
encryptedCouchDBConnection: "",
encryptedPassphrase: "",
} as Partial<ObsidianLiveSyncSettings>;
if (stripExtra) {
delete setting.pluginSyncExtendedSetting;
}
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
for (const k of keys) {
if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) {
if (
JSON.stringify(k in setting ? setting[k] : "") ==
JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")
) {
delete setting[k];
}
}
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false));
const encryptedSetting = encodeURIComponent(
await encrypt(JSON.stringify(setting), encryptingPassphrase, false)
);
const uri = `${configURIBase}${encryptedSetting}`;
await navigator.clipboard.writeText(uri);
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
}
async command_copySetupURIFull() {
const encryptingPassphrase = await this.core.confirm.askString("Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
if (encryptingPassphrase === false)
return;
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false));
const encryptingPassphrase = await this.core.confirm.askString(
"Encrypt your settings",
"The passphrase to encrypt the setup URI",
"",
true
);
if (encryptingPassphrase === false) return;
const setting = {
...this.settings,
configPassphraseStore: "",
encryptedCouchDBConnection: "",
encryptedPassphrase: "",
};
const encryptedSetting = encodeURIComponent(
await encrypt(JSON.stringify(setting), encryptingPassphrase, false)
);
const uri = `${configURIBase}${encryptedSetting}`;
await navigator.clipboard.writeText(uri);
this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
@@ -72,8 +104,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
}
async command_openSetupURI() {
const setupURI = await this.core.confirm.askString("Easy setup", "Set up URI", `${configURIBase}aaaaa`);
if (setupURI === false)
return;
if (setupURI === false) return;
if (!setupURI.startsWith(`${configURIBase}`)) {
this._log("Set up URI looks wrong.", LOG_LEVEL_NOTICE);
return;
@@ -85,12 +116,19 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
async setupWizard(confString: string) {
try {
const oldConf = JSON.parse(JSON.stringify(this.settings));
const encryptingPassphrase = await this.core.confirm.askString("Passphrase", "The passphrase to decrypt your setup URI", "", true);
if (encryptingPassphrase === false)
return;
const encryptingPassphrase = await this.core.confirm.askString(
"Passphrase",
"The passphrase to decrypt your setup URI",
"",
true
);
if (encryptingPassphrase === false) return;
const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false));
if (newConf) {
const result = await this.core.confirm.askYesNoDialog("Importing Configuration from the Setup-URI. Are you sure to proceed?", {});
const result = await this.core.confirm.askYesNoDialog(
"Importing Configuration from the Setup-URI. Are you sure to proceed?",
{}
);
if (result == "yes") {
const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf) as ObsidianLiveSyncSettings;
this.core.replicator.closeReplication();
@@ -100,7 +138,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
newSettingW.configPassphraseStore = "";
newSettingW.encryptedPassphrase = "";
newSettingW.encryptedCouchDBConnection = "";
newSettingW.additionalSuffixOfDatabaseName = `${("appId" in this.app ? this.app.appId : "")}`
newSettingW.additionalSuffixOfDatabaseName = `${"appId" in this.app ? this.app.appId : ""}`;
const setupJustImport = "Just import setting";
const setupAsNew = "Set it up as secondary or subsequent device";
const setupAsMerge = "Secondary device but try keeping local changes";
@@ -114,7 +152,11 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
newSettingW.useIndexedDBAdapter = true;
}
const setupType = await this.core.confirm.askSelectStringDialogue("How would you like to set it up?", [setupAsNew, setupAgain, setupAsMerge, setupJustImport, setupManually], { defaultAction: setupAsNew });
const setupType = await this.core.confirm.askSelectStringDialogue(
"How would you like to set it up?",
[setupAsNew, setupAgain, setupAsMerge, setupJustImport, setupManually],
{ defaultAction: setupAsNew }
);
if (setupType == setupJustImport) {
this.core.settings = newSettingW;
this.core.$$clearUsedPassphrase();
@@ -128,16 +170,27 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
this.core.$$clearUsedPassphrase();
await this.core.rebuilder.$fetchLocal(true);
} else if (setupType == setupAgain) {
const confirm = "I know this operation will rebuild all my databases with files on this device, and files that are on the remote database and I didn't synchronize to any other devices will be lost and want to proceed indeed.";
if (await this.core.confirm.askSelectStringDialogue("Do you really want to do this?", ["Cancel", confirm], { defaultAction: "Cancel" }) != confirm) {
const confirm =
"I know this operation will rebuild all my databases with files on this device, and files that are on the remote database and I didn't synchronize to any other devices will be lost and want to proceed indeed.";
if (
(await this.core.confirm.askSelectStringDialogue(
"Do you really want to do this?",
["Cancel", confirm],
{ defaultAction: "Cancel" }
)) != confirm
) {
return;
}
this.core.settings = newSettingW;
this.core.$$clearUsedPassphrase();
await this.core.rebuilder.$rebuildEverything();
} else if (setupType == setupManually) {
const keepLocalDB = await this.core.confirm.askYesNoDialog("Keep local DB?", { defaultOption: "No" });
const keepRemoteDB = await this.core.confirm.askYesNoDialog("Keep remote DB?", { defaultOption: "No" });
const keepLocalDB = await this.core.confirm.askYesNoDialog("Keep local DB?", {
defaultOption: "No",
});
const keepRemoteDB = await this.core.confirm.askYesNoDialog("Keep remote DB?", {
defaultOption: "No",
});
if (keepLocalDB == "yes" && keepRemoteDB == "yes") {
// nothing to do. so peaceful.
this.core.settings = newSettingW;
@@ -145,7 +198,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
await this.core.$allSuspendAllSync();
await this.core.$allSuspendExtraSync();
await this.core.saveSettings();
const replicate = await this.core.confirm.askYesNoDialog("Unlock and replicate?", { defaultOption: "Yes" });
const replicate = await this.core.confirm.askYesNoDialog("Unlock and replicate?", {
defaultOption: "Yes",
});
if (replicate == "yes") {
await this.core.$$replicate(true);
await this.core.$$markRemoteUnlocked();
@@ -154,7 +209,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
return;
}
if (keepLocalDB == "no" && keepRemoteDB == "no") {
const reset = await this.core.confirm.askYesNoDialog("Drop everything?", { defaultOption: "No" });
const reset = await this.core.confirm.askYesNoDialog("Drop everything?", {
defaultOption: "No",
});
if (reset != "yes") {
this._log("Cancelled", LOG_LEVEL_NOTICE);
this.core.settings = oldConf;
@@ -168,7 +225,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
if (keepLocalDB == "no") {
await this.core.$$resetLocalDatabase();
await this.core.localDatabase.initializeDatabase();
const rebuild = await this.core.confirm.askYesNoDialog("Rebuild the database?", { defaultOption: "Yes" });
const rebuild = await this.core.confirm.askYesNoDialog("Rebuild the database?", {
defaultOption: "Yes",
});
if (rebuild == "yes") {
initDB = this.core.$$initializeDatabase(true);
} else {
@@ -180,7 +239,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
await this.core.$$markRemoteLocked();
}
if (keepLocalDB == "no" || keepRemoteDB == "no") {
const replicate = await this.core.confirm.askYesNoDialog("Replicate once?", { defaultOption: "Yes" });
const replicate = await this.core.confirm.askYesNoDialog("Replicate once?", {
defaultOption: "Yes",
});
if (replicate == "yes") {
if (initDB != null) {
await initDB;
@@ -200,6 +261,4 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi
this._log(ex, LOG_LEVEL_VERBOSE);
}
}
}

View File

@@ -1,9 +1,35 @@
import { Setting, TextComponent, type ToggleComponent, type DropdownComponent, ButtonComponent, type TextAreaComponent, type ValueComponent } from "obsidian";
import {
Setting,
TextComponent,
type ToggleComponent,
type DropdownComponent,
ButtonComponent,
type TextAreaComponent,
type ValueComponent,
} from "obsidian";
import { unique } from "octagonal-wheels/collection";
import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "../../../lib/src/common/types.ts";
import { type ObsidianLiveSyncSettingTab, type AutoWireOption, wrapMemo, type OnUpdateResult, createStub, findAttrFromParent } from "./ObsidianLiveSyncSettingTab.ts";
import { type AllSettingItemKey, getConfig, type AllSettings, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey } from "./settingConstants.ts";
import {
LEVEL_ADVANCED,
LEVEL_POWER_USER,
statusDisplay,
type ConfigurationItem,
} from "../../../lib/src/common/types.ts";
import {
type ObsidianLiveSyncSettingTab,
type AutoWireOption,
wrapMemo,
type OnUpdateResult,
createStub,
findAttrFromParent,
} from "./ObsidianLiveSyncSettingTab.ts";
import {
type AllSettingItemKey,
getConfig,
type AllSettings,
type AllStringItemKey,
type AllNumericItemKey,
type AllBooleanItemKey,
} from "./settingConstants.ts";
export class LiveSyncSetting extends Setting {
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
@@ -29,8 +55,9 @@ export class LiveSyncSetting extends Setting {
DEV: {
const paneName = findAttrFromParent(this.settingEl, "data-pane");
const panelName = findAttrFromParent(this.settingEl, "data-panel");
const itemName = typeof this.nameBuf == "string" ? this.nameBuf : this.nameBuf.textContent?.toString() ?? "";
const strValue = typeof value == "string" ? value : value.textContent?.toString() ?? "";
const itemName =
typeof this.nameBuf == "string" ? this.nameBuf : (this.nameBuf.textContent?.toString() ?? "");
const strValue = typeof value == "string" ? value : (value.textContent?.toString() ?? "");
createStub(itemName, key, strValue, panelName, paneName);
}
@@ -111,9 +138,11 @@ export class LiveSyncSetting extends Setting {
}
autoWireText(key: AllStringItemKey, opt?: AutoWireOption) {
const conf = this.autoWireSetting(key, opt);
this.addText(text => {
this.addText((text) => {
this.autoWiredComponent = text;
const setValue = wrapMemo((value: string) => { text.setValue(value) });
const setValue = wrapMemo((value: string) => {
text.setValue(value);
});
this.invalidateValue = () => setValue(`${LiveSyncSetting.env.editingSettings[key]}`);
this.invalidateValue();
text.onChange(async (value) => {
@@ -129,9 +158,11 @@ export class LiveSyncSetting extends Setting {
}
autoWireTextArea(key: AllStringItemKey, opt?: AutoWireOption) {
const conf = this.autoWireSetting(key, opt);
this.addTextArea(text => {
this.addTextArea((text) => {
this.autoWiredComponent = text;
const setValue = wrapMemo((value: string) => { text.setValue(value) });
const setValue = wrapMemo((value: string) => {
text.setValue(value);
});
this.invalidateValue = () => setValue(`${LiveSyncSetting.env.editingSettings[key]}`);
this.invalidateValue();
text.onChange(async (value) => {
@@ -145,9 +176,12 @@ export class LiveSyncSetting extends Setting {
});
return this;
}
autoWireNumeric(key: AllNumericItemKey, opt: AutoWireOption & { clampMin?: number; clampMax?: number; acceptZero?: boolean; }) {
autoWireNumeric(
key: AllNumericItemKey,
opt: AutoWireOption & { clampMin?: number; clampMax?: number; acceptZero?: boolean }
) {
const conf = this.autoWireSetting(key, opt);
this.addText(text => {
this.addText((text) => {
this.autoWiredComponent = text;
if (opt.clampMin) {
text.inputEl.setAttribute("min", `${opt.clampMin}`);
@@ -156,7 +190,9 @@ export class LiveSyncSetting extends Setting {
text.inputEl.setAttribute("max", `${opt.clampMax}`);
}
let lastError = false;
const setValue = wrapMemo((value: string) => { text.setValue(value) });
const setValue = wrapMemo((value: string) => {
text.setValue(value);
});
this.invalidateValue = () => {
if (!lastError) setValue(`${LiveSyncSetting.env.editingSettings[key]}`);
};
@@ -193,9 +229,11 @@ export class LiveSyncSetting extends Setting {
}
autoWireToggle(key: AllBooleanItemKey, opt?: AutoWireOption) {
const conf = this.autoWireSetting(key, opt);
this.addToggle(toggle => {
this.addToggle((toggle) => {
this.autoWiredComponent = toggle;
const setValue = wrapMemo((value: boolean) => { toggle.setValue(opt?.invert ? !value : value) });
const setValue = wrapMemo((value: boolean) => {
toggle.setValue(opt?.invert ? !value : value);
});
this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] ?? false);
this.invalidateValue();
@@ -207,16 +245,15 @@ export class LiveSyncSetting extends Setting {
});
return this;
}
autoWireDropDown<T extends string>(key: AllStringItemKey, opt: AutoWireOption & { options: Record<T, string>; }) {
autoWireDropDown<T extends string>(key: AllStringItemKey, opt: AutoWireOption & { options: Record<T, string> }) {
const conf = this.autoWireSetting(key, opt);
this.addDropdown(dropdown => {
this.addDropdown((dropdown) => {
this.autoWiredComponent = dropdown;
const setValue = wrapMemo((value: string) => {
dropdown.setValue(value);
});
dropdown
.addOptions(opt.options);
dropdown.addOptions(opt.options);
this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] || "");
this.invalidateValue();
@@ -264,7 +301,6 @@ export class LiveSyncSetting extends Setting {
const newConf = this._getComputedStatus();
const keys = Object.keys(newConf) as [keyof OnUpdateResult];
for (const k of keys) {
if (k in this.prevStatus && this.prevStatus[k] == newConf[k]) {
continue;
}
@@ -277,8 +313,8 @@ export class LiveSyncSetting extends Setting {
case "classes":
break;
case "disabled":
this.setDisabled((newConf[k] || false));
this.settingEl.toggleClass("sls-setting-disabled", (newConf[k] || false));
this.setDisabled(newConf[k] || false);
this.settingEl.toggleClass("sls-setting-disabled", newConf[k] || false);
this.prevStatus[k] = newConf[k];
break;
case "isCta":

View File

@@ -1,13 +1,21 @@
import { $t } from "../../../lib/src/common/i18n.ts";
import { DEFAULT_SETTINGS, configurationNames, type ConfigurationItem, type FilterBooleanKeys, type FilterNumberKeys, type FilterStringKeys, type ObsidianLiveSyncSettings } from "../../../lib/src/common/types.ts";
import {
DEFAULT_SETTINGS,
configurationNames,
type ConfigurationItem,
type FilterBooleanKeys,
type FilterNumberKeys,
type FilterStringKeys,
type ObsidianLiveSyncSettings,
} from "../../../lib/src/common/types.ts";
export type OnDialogSettings = {
configPassphrase: string,
preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE",
syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"
dummy: number,
deviceAndVaultName: string,
}
configPassphrase: string;
preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE";
syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC";
dummy: number;
deviceAndVaultName: string;
};
export const OnDialogSettingsDefault: OnDialogSettings = {
configPassphrase: "",
@@ -15,9 +23,8 @@ export const OnDialogSettingsDefault: OnDialogSettings = {
syncMode: "ONEVENTS",
dummy: 0,
deviceAndVaultName: "",
}
export const AllSettingDefault =
{ ...DEFAULT_SETTINGS, ...OnDialogSettingsDefault }
};
export const AllSettingDefault = { ...DEFAULT_SETTINGS, ...OnDialogSettingsDefault };
export type AllSettings = ObsidianLiveSyncSettings & OnDialogSettings;
export type AllStringItemKey = FilterStringKeys<AllSettings>;
@@ -25,324 +32,326 @@ export type AllNumericItemKey = FilterNumberKeys<AllSettings>;
export type AllBooleanItemKey = FilterBooleanKeys<AllSettings>;
export type AllSettingItemKey = AllStringItemKey | AllNumericItemKey | AllBooleanItemKey;
export type ValueOf<T extends AllSettingItemKey> =
T extends AllStringItemKey ? string :
T extends AllNumericItemKey ? number :
T extends AllBooleanItemKey ? boolean :
AllSettings[T];
export type ValueOf<T extends AllSettingItemKey> = T extends AllStringItemKey
? string
: T extends AllNumericItemKey
? number
: T extends AllBooleanItemKey
? boolean
: AllSettings[T];
export const SettingInformation: Partial<Record<keyof AllSettings, ConfigurationItem>> = {
"liveSync": {
"name": "Sync Mode"
},
"couchDB_URI": {
"name": "URI",
"placeHolder": "https://........"
},
"couchDB_USER": {
"name": "Username",
"desc": "username"
},
"couchDB_PASSWORD": {
"name": "Password",
"desc": "password"
},
"couchDB_DBNAME": {
"name": "Database name"
},
"passphrase": {
"name": "Passphrase",
"desc": "Encrypting passphrase. If you change the passphrase of an existing database, overwriting the remote database is strongly recommended."
},
"showStatusOnEditor": {
"name": "Show status inside the editor",
"desc": "Reflected after reboot"
},
"showOnlyIconsOnEditor": {
"name": "Show status as icons only"
},
"showStatusOnStatusbar": {
"name": "Show status on the status bar",
"desc": "Reflected after reboot."
},
"lessInformationInLog": {
"name": "Show only notifications",
"desc": "Prevent logging and show only notification. Please disable when you report the logs"
},
"showVerboseLog": {
"name": "Verbose Log",
"desc": "Show verbose log. Please enable when you report the logs"
},
"hashCacheMaxCount": {
"name": "Memory cache size (by total items)"
},
"hashCacheMaxAmount": {
"name": "Memory cache size (by total characters)",
"desc": "(Mega chars)"
},
"writeCredentialsForSettingSync": {
"name": "Write credentials in the file",
"desc": "(Not recommended) If set, credentials will be stored in the file."
},
"notifyAllSettingSyncFile": {
"name": "Notify all setting files"
},
"configPassphrase": {
"name": "Passphrase of sensitive configuration items",
"desc": "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again."
},
"configPassphraseStore": {
"name": "Encrypting sensitive configuration items"
},
"syncOnSave": {
"name": "Sync on Save",
"desc": "When you save a file, sync automatically"
},
"syncOnEditorSave": {
"name": "Sync on Editor Save",
"desc": "When you save a file in the editor, sync automatically"
},
"syncOnFileOpen": {
"name": "Sync on File Open",
"desc": "When you open a file, sync automatically"
},
"syncOnStart": {
"name": "Sync on Start",
"desc": "Start synchronization after launching Obsidian."
},
"syncAfterMerge": {
"name": "Sync after merging file",
"desc": "Sync automatically after merging files"
},
"trashInsteadDelete": {
"name": "Use the trash bin",
"desc": "Do not delete files that are deleted in remote, just move to trash."
},
"doNotDeleteFolder": {
"name": "Keep empty folder",
"desc": "Normally, a folder is deleted when it becomes empty after a synchronization. Enabling this will prevent it from getting deleted"
},
"resolveConflictsByNewerFile": {
"name": "Always overwrite with a newer file (beta)",
"desc": "(Def off) Resolve conflicts by newer files automatically."
},
"checkConflictOnlyOnOpen": {
"name": "Postpone resolution of inactive files"
},
"showMergeDialogOnlyOnActive": {
"name": "Postpone manual resolution of inactive files"
},
"disableMarkdownAutoMerge": {
"name": "Always resolve conflicts manually",
"desc": "If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)"
},
"writeDocumentsIfConflicted": {
"name": "Always reflect synchronized changes even if the note has a conflict",
"desc": "Turn on to previous behavior"
},
"syncInternalFilesInterval": {
"name": "Scan hidden files periodically",
"desc": "Seconds, 0 to disable"
},
"batchSave": {
"name": "Batch database update",
"desc": "Reducing the frequency with which on-disk changes are reflected into the DB"
},
"readChunksOnline": {
"name": "Fetch chunks on demand",
"desc": "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended."
},
"syncMaxSizeInMB": {
"name": "Maximum file size",
"desc": "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used."
},
"useIgnoreFiles": {
"name": "(Beta) Use ignore files",
"desc": "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files."
},
"ignoreFiles": {
"name": "Ignore files",
"desc": "We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`"
},
"batch_size": {
"name": "Batch size",
"desc": "Number of change feed items to process at a time. Defaults to 50. Minimum is 2."
},
"batches_limit": {
"name": "Batch limit",
"desc": "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time."
},
"useTimeouts": {
"name": "Use timeouts instead of heartbeats",
"desc": "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage."
},
"concurrencyOfReadChunksOnline": {
"name": "Batch size of on-demand fetching"
},
"minimumIntervalOfReadChunksOnline": {
"name": "The delay for consecutive on-demand fetches"
},
"suspendFileWatching": {
"name": "Suspend file watching",
"desc": "Stop watching for file change."
},
"suspendParseReplicationResult": {
"name": "Suspend database reflecting",
"desc": "Stop reflecting database changes to storage files."
},
"writeLogToTheFile": {
"name": "Write logs into the file",
"desc": "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information."
},
"deleteMetadataOfDeletedFiles": {
"name": "Do not keep metadata of deleted files."
},
"useIndexedDBAdapter": {
"name": "(Obsolete) Use an old adapter for compatibility",
"desc": "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.",
"obsolete": true
},
"watchInternalFileChanges": {
"name": "Scan changes on customization sync",
"desc": "Do not use internal API"
},
"doNotSuspendOnFetching": {
"name": "Fetch database with previous behaviour"
},
"disableCheckingConfigMismatch": {
"name": "Do not check configuration mismatch before replication"
},
"usePluginSync": {
"name": "Enable customization sync"
},
"autoSweepPlugins": {
"name": "Scan customization automatically",
"desc": "Scan customization before replicating."
},
"autoSweepPluginsPeriodic": {
"name": "Scan customization periodically",
"desc": "Scan customization every 1 minute."
},
"notifyPluginOrSettingUpdated": {
"name": "Notify customized",
"desc": "Notify when other device has newly customized."
},
"remoteType": {
"name": "Remote Type",
"desc": "Remote server type"
},
"endpoint": {
"name": "Endpoint URL",
"placeHolder": "https://........"
},
"accessKey": {
"name": "Access Key"
},
"secretKey": {
"name": "Secret Key"
},
"region": {
"name": "Region",
"placeHolder": "auto"
},
"bucket": {
"name": "Bucket Name"
},
"useCustomRequestHandler": {
"name": "Use Custom HTTP Handler",
"desc": "If your Object Storage could not configured accepting CORS, enable this."
},
"maxChunksInEden": {
"name": "Maximum Incubating Chunks",
"desc": "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks."
},
"maxTotalLengthInEden": {
"name": "Maximum Incubating Chunk Size",
"desc": "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks."
},
"maxAgeInEden": {
"name": "Maximum Incubation Period",
"desc": "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks."
},
"settingSyncFile": {
"name": "Filename",
"desc": "If you set this, all settings are saved in a markdown file. You will be notified when new settings arrive. You can set different files by the platform."
},
"preset": {
"name": "Presets",
"desc": "Apply preset configuration"
},
"syncMode": {
liveSync: {
name: "Sync Mode",
},
"periodicReplicationInterval": {
"name": "Periodic Sync interval",
"desc": "Interval (sec)"
couchDB_URI: {
name: "URI",
placeHolder: "https://........",
},
"syncInternalFilesBeforeReplication": {
"name": "Scan for hidden files before replication"
couchDB_USER: {
name: "Username",
desc: "username",
},
"automaticallyDeleteMetadataOfDeletedFiles": {
"name": "Delete old metadata of deleted files on start-up",
"desc": "(Days passed, 0 to disable automatic-deletion)"
couchDB_PASSWORD: {
name: "Password",
desc: "password",
},
"additionalSuffixOfDatabaseName": {
"name": "Database suffix",
"desc": "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured."
couchDB_DBNAME: {
name: "Database name",
},
"hashAlg": {
"name": configurationNames["hashAlg"]?.name || "",
"desc": "xxhash64 is the current default."
passphrase: {
name: "Passphrase",
desc: "Encrypting passphrase. If you change the passphrase of an existing database, overwriting the remote database is strongly recommended.",
},
"deviceAndVaultName": {
"name": "Device name",
"desc": "Unique name between all synchronized devices. To edit this setting, please disable customization sync once."
showStatusOnEditor: {
name: "Show status inside the editor",
desc: "Reflected after reboot",
},
"displayLanguage": {
"name": "Display Language",
"desc": "Not all messages have been translated. And, please revert to \"Default\" when reporting errors."
showOnlyIconsOnEditor: {
name: "Show status as icons only",
},
showStatusOnStatusbar: {
name: "Show status on the status bar",
desc: "Reflected after reboot.",
},
lessInformationInLog: {
name: "Show only notifications",
desc: "Prevent logging and show only notification. Please disable when you report the logs",
},
showVerboseLog: {
name: "Verbose Log",
desc: "Show verbose log. Please enable when you report the logs",
},
hashCacheMaxCount: {
name: "Memory cache size (by total items)",
},
hashCacheMaxAmount: {
name: "Memory cache size (by total characters)",
desc: "(Mega chars)",
},
writeCredentialsForSettingSync: {
name: "Write credentials in the file",
desc: "(Not recommended) If set, credentials will be stored in the file.",
},
notifyAllSettingSyncFile: {
name: "Notify all setting files",
},
configPassphrase: {
name: "Passphrase of sensitive configuration items",
desc: "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.",
},
configPassphraseStore: {
name: "Encrypting sensitive configuration items",
},
syncOnSave: {
name: "Sync on Save",
desc: "When you save a file, sync automatically",
},
syncOnEditorSave: {
name: "Sync on Editor Save",
desc: "When you save a file in the editor, sync automatically",
},
syncOnFileOpen: {
name: "Sync on File Open",
desc: "When you open a file, sync automatically",
},
syncOnStart: {
name: "Sync on Start",
desc: "Start synchronization after launching Obsidian.",
},
syncAfterMerge: {
name: "Sync after merging file",
desc: "Sync automatically after merging files",
},
trashInsteadDelete: {
name: "Use the trash bin",
desc: "Do not delete files that are deleted in remote, just move to trash.",
},
doNotDeleteFolder: {
name: "Keep empty folder",
desc: "Normally, a folder is deleted when it becomes empty after a synchronization. Enabling this will prevent it from getting deleted",
},
resolveConflictsByNewerFile: {
name: "Always overwrite with a newer file (beta)",
desc: "(Def off) Resolve conflicts by newer files automatically.",
},
checkConflictOnlyOnOpen: {
name: "Postpone resolution of inactive files",
},
showMergeDialogOnlyOnActive: {
name: "Postpone manual resolution of inactive files",
},
disableMarkdownAutoMerge: {
name: "Always resolve conflicts manually",
desc: "If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)",
},
writeDocumentsIfConflicted: {
name: "Always reflect synchronized changes even if the note has a conflict",
desc: "Turn on to previous behavior",
},
syncInternalFilesInterval: {
name: "Scan hidden files periodically",
desc: "Seconds, 0 to disable",
},
batchSave: {
name: "Batch database update",
desc: "Reducing the frequency with which on-disk changes are reflected into the DB",
},
readChunksOnline: {
name: "Fetch chunks on demand",
desc: "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.",
},
syncMaxSizeInMB: {
name: "Maximum file size",
desc: "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.",
},
useIgnoreFiles: {
name: "(Beta) Use ignore files",
desc: "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.",
},
ignoreFiles: {
name: "Ignore files",
desc: "We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`",
},
batch_size: {
name: "Batch size",
desc: "Number of change feed items to process at a time. Defaults to 50. Minimum is 2.",
},
batches_limit: {
name: "Batch limit",
desc: "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.",
},
useTimeouts: {
name: "Use timeouts instead of heartbeats",
desc: "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.",
},
concurrencyOfReadChunksOnline: {
name: "Batch size of on-demand fetching",
},
minimumIntervalOfReadChunksOnline: {
name: "The delay for consecutive on-demand fetches",
},
suspendFileWatching: {
name: "Suspend file watching",
desc: "Stop watching for file change.",
},
suspendParseReplicationResult: {
name: "Suspend database reflecting",
desc: "Stop reflecting database changes to storage files.",
},
writeLogToTheFile: {
name: "Write logs into the file",
desc: "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.",
},
deleteMetadataOfDeletedFiles: {
name: "Do not keep metadata of deleted files.",
},
useIndexedDBAdapter: {
name: "(Obsolete) Use an old adapter for compatibility",
desc: "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.",
obsolete: true,
},
watchInternalFileChanges: {
name: "Scan changes on customization sync",
desc: "Do not use internal API",
},
doNotSuspendOnFetching: {
name: "Fetch database with previous behaviour",
},
disableCheckingConfigMismatch: {
name: "Do not check configuration mismatch before replication",
},
usePluginSync: {
name: "Enable customization sync",
},
autoSweepPlugins: {
name: "Scan customization automatically",
desc: "Scan customization before replicating.",
},
autoSweepPluginsPeriodic: {
name: "Scan customization periodically",
desc: "Scan customization every 1 minute.",
},
notifyPluginOrSettingUpdated: {
name: "Notify customized",
desc: "Notify when other device has newly customized.",
},
remoteType: {
name: "Remote Type",
desc: "Remote server type",
},
endpoint: {
name: "Endpoint URL",
placeHolder: "https://........",
},
accessKey: {
name: "Access Key",
},
secretKey: {
name: "Secret Key",
},
region: {
name: "Region",
placeHolder: "auto",
},
bucket: {
name: "Bucket Name",
},
useCustomRequestHandler: {
name: "Use Custom HTTP Handler",
desc: "If your Object Storage could not configured accepting CORS, enable this.",
},
maxChunksInEden: {
name: "Maximum Incubating Chunks",
desc: "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.",
},
maxTotalLengthInEden: {
name: "Maximum Incubating Chunk Size",
desc: "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.",
},
maxAgeInEden: {
name: "Maximum Incubation Period",
desc: "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.",
},
settingSyncFile: {
name: "Filename",
desc: "If you set this, all settings are saved in a markdown file. You will be notified when new settings arrive. You can set different files by the platform.",
},
preset: {
name: "Presets",
desc: "Apply preset configuration",
},
syncMode: {
name: "Sync Mode",
},
periodicReplicationInterval: {
name: "Periodic Sync interval",
desc: "Interval (sec)",
},
syncInternalFilesBeforeReplication: {
name: "Scan for hidden files before replication",
},
automaticallyDeleteMetadataOfDeletedFiles: {
name: "Delete old metadata of deleted files on start-up",
desc: "(Days passed, 0 to disable automatic-deletion)",
},
additionalSuffixOfDatabaseName: {
name: "Database suffix",
desc: "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.",
},
hashAlg: {
name: configurationNames["hashAlg"]?.name || "",
desc: "xxhash64 is the current default.",
},
deviceAndVaultName: {
name: "Device name",
desc: "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.",
},
displayLanguage: {
name: "Display Language",
desc: 'Not all messages have been translated. And, please revert to "Default" when reporting errors.',
},
enableChunkSplitterV2: {
name: "Use splitting-limit-capped chunk splitter",
desc: "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker."
desc: "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.",
},
disableWorkerForGeneratingChunks: {
name: "Do not split chunks in the background",
desc: "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour)."
desc: "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).",
},
processSmallFilesInUIThread: {
name: "Process small files in the foreground",
desc: "If enabled, the file under 1kb will be processed in the UI thread."
desc: "If enabled, the file under 1kb will be processed in the UI thread.",
},
batchSaveMinimumDelay: {
name: "Minimum delay for batch database updating",
desc: "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving."
desc: "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.",
},
batchSaveMaximumDelay: {
name: "Maximum delay for batch database updating",
desc: "Saving will be performed forcefully after this number of seconds."
desc: "Saving will be performed forcefully after this number of seconds.",
},
"notifyThresholdOfRemoteStorageSize": {
notifyThresholdOfRemoteStorageSize: {
name: "Notify when the estimated remote storage size exceeds on start up",
desc: "MB (0 to disable)."
desc: "MB (0 to disable).",
},
"usePluginSyncV2": {
name: "Enable per-file-saved customization sync",
desc: "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions."
usePluginSyncV2: {
name: "Enable per-file customization sync",
desc: "If enabled, efficient per-file customization sync will be used. A minor migration is required when enabling this feature, and all devices must be updated to v0.23.18. Enabling this feature will result in losing compatibility with older versions.",
},
"handleFilenameCaseSensitive": {
handleFilenameCaseSensitive: {
name: "Handle files as Case-Sensitive",
desc: "If this enabled, All files are handled as case-Sensitive (Previous behaviour)."
desc: "If this enabled, All files are handled as case-Sensitive (Previous behaviour).",
},
"doNotUseFixedRevisionForChunks": {
doNotUseFixedRevisionForChunks: {
name: "Compute revisions for chunks (Previous behaviour)",
desc: "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)"
desc: "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)",
},
"sendChunksBulkMaxSize": {
sendChunksBulkMaxSize: {
name: "Maximum size of chunks to send in one request",
desc: "MB"
desc: "MB",
},
"useAdvancedMode": {
useAdvancedMode: {
name: "Enable advanced features",
// desc: "Enable advanced mode"
},
@@ -354,11 +363,11 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
useEdgeCaseMode: {
name: "Enable edge case treatment features",
},
"enableDebugTools": {
enableDebugTools: {
name: "Enable Developers' Debug Tools.",
desc: "You need a restart to apply this setting."
}
}
desc: "You need a restart to apply this setting.",
},
};
function translateInfo(infoSrc: ConfigurationItem | undefined | false) {
if (!infoSrc) return false;
const info = { ...infoSrc };
@@ -369,7 +378,6 @@ function translateInfo(infoSrc: ConfigurationItem | undefined | false) {
return info;
}
function _getConfig(key: AllSettingItemKey) {
if (key in configurationNames) {
return configurationNames[key as keyof ObsidianLiveSyncSettings];
}
@@ -385,4 +393,4 @@ export function getConfName(key: AllSettingItemKey) {
const conf = getConfig(key);
if (!conf) return `${key} (No info)`;
return conf.name;
}
}

View File

@@ -2,13 +2,26 @@ export interface Confirm {
askYesNo(message: string): Promise<"yes" | "no">;
askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise<string | false>;
askYesNoDialog(message: string, opt: { title?: string, defaultOption?: "Yes" | "No", timeout?: number }): Promise<"yes" | "no">;
askYesNoDialog(
message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number }
): Promise<"yes" | "no">;
askSelectString(message: string, items: string[]): Promise<string>
askSelectString(message: string, items: string[]): Promise<string>;
askSelectStringDialogue(message: string, buttons: string[], opt: { title?: string, defaultAction: (typeof buttons)[number], timeout?: number }): Promise<(typeof buttons)[number] | false>;
askSelectStringDialogue(
message: string,
buttons: string[],
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number }
): Promise<(typeof buttons)[number] | false>;
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void;
confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>;
}
confirmWithMessage(
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout?: number
): Promise<(typeof buttons)[number] | false>;
}

View File

@@ -1,18 +1,34 @@
import type { FilePathWithPrefix, LoadedEntry, MetaEntry, UXFileInfo, UXFileInfoStub } from "../../lib/src/common/types";
import type {
FilePathWithPrefix,
LoadedEntry,
MetaEntry,
UXFileInfo,
UXFileInfoStub,
} from "../../lib/src/common/types";
export interface DatabaseFileAccess {
delete: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string) => Promise<boolean>;
store: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise<boolean>;
storeContent(path: FilePathWithPrefix, content: string): Promise<boolean>;
createChunks: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise<boolean>;
fetch: (file: UXFileInfoStub | FilePathWithPrefix,
rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise<UXFileInfo | false>;
fetchEntryFromMeta: (meta: MetaEntry,
waitForReady?: boolean, skipCheck?: boolean) => Promise<LoadedEntry | false>;
fetchEntryMeta: (file: UXFileInfoStub | FilePathWithPrefix,
rev?: string, skipCheck?: boolean) => Promise<MetaEntry | false>;
fetchEntry: (file: UXFileInfoStub | FilePathWithPrefix,
rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise<LoadedEntry | false>;
fetch: (
file: UXFileInfoStub | FilePathWithPrefix,
rev?: string,
waitForReady?: boolean,
skipCheck?: boolean
) => Promise<UXFileInfo | false>;
fetchEntryFromMeta: (meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean) => Promise<LoadedEntry | false>;
fetchEntryMeta: (
file: UXFileInfoStub | FilePathWithPrefix,
rev?: string,
skipCheck?: boolean
) => Promise<MetaEntry | false>;
fetchEntry: (
file: UXFileInfoStub | FilePathWithPrefix,
rev?: string,
waitForReady?: boolean,
skipCheck?: boolean
) => Promise<LoadedEntry | false>;
getConflictedRevs: (file: UXFileInfoStub | FilePathWithPrefix) => Promise<string[]>;
// storeFromStorage: (file: UXFileInfoStub | FilePathWithPrefix, force?: boolean) => Promise<boolean>;
}
}

View File

@@ -1,5 +1,7 @@
export interface Rebuilder {
$performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise<void>;
$performRebuildDB(
method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"
): Promise<void>;
$rebuildRemote(): Promise<void>;
$rebuildEverything(): Promise<void>;
$fetchLocal(makeLocalChunkBeforeSync?: boolean): Promise<void>;
@@ -7,5 +9,4 @@ export interface Rebuilder {
scheduleRebuild(): Promise<void>;
scheduleFetch(): Promise<void>;
resolveAllConflictedFilesByNewerOnes(): Promise<void>;
}
}

View File

@@ -1,43 +1,50 @@
import type { FilePath, FilePathWithPrefix, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXStat } from "../../lib/src/common/types"
import type {
FilePath,
FilePathWithPrefix,
UXDataWriteOptions,
UXFileInfo,
UXFileInfoStub,
UXFolderInfo,
UXStat,
} from "../../lib/src/common/types";
export interface StorageAccess {
deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise<void>;
deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise<void>
writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean>;
writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean>
readFileAuto(path: string): Promise<string | ArrayBuffer>;
readFileText(path: string): Promise<string>;
isExists(path: string): Promise<boolean>;
writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean>;
appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise<boolean>;
readFileAuto(path: string): Promise<string | ArrayBuffer>
readFileText(path: string): Promise<string>
isExists(path: string): Promise<boolean>
writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean>
appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise<boolean>
stat(path: string): Promise<UXStat | null>
statHidden(path: string): Promise<UXStat | null>
removeHidden(path: string): Promise<boolean>
readHiddenFileAuto(path: string): Promise<string | ArrayBuffer>
readHiddenFileBinary(path: string): Promise<ArrayBuffer>
readHiddenFileText(path: string): Promise<string>
isExistsIncludeHidden(path: string): Promise<boolean>
stat(path: string): Promise<UXStat | null>;
statHidden(path: string): Promise<UXStat | null>;
removeHidden(path: string): Promise<boolean>;
readHiddenFileAuto(path: string): Promise<string | ArrayBuffer>;
readHiddenFileBinary(path: string): Promise<ArrayBuffer>;
readHiddenFileText(path: string): Promise<string>;
isExistsIncludeHidden(path: string): Promise<boolean>;
// This could be work also for the hidden files.
ensureDir(path: string): Promise<boolean>
triggerFileEvent(event: string, path: string): void
triggerHiddenFile(path: string): Promise<void>
ensureDir(path: string): Promise<boolean>;
triggerFileEvent(event: string, path: string): void;
triggerHiddenFile(path: string): Promise<void>;
getFileStub(path: string): UXFileInfoStub | null
getFileStub(path: string): UXFileInfoStub | null;
readStubContent(stub: UXFileInfoStub): Promise<UXFileInfo | false>;
getStub(path: string): UXFileInfoStub | UXFolderInfo | null
getStub(path: string): UXFileInfoStub | UXFolderInfo | null;
getFiles(): UXFileInfoStub[]
getFileNames(): FilePathWithPrefix[]
getFiles(): UXFileInfoStub[];
getFileNames(): FilePathWithPrefix[];
touched(file: UXFileInfoStub | FilePathWithPrefix): void
recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean
clearTouched(): void
touched(file: UXFileInfoStub | FilePathWithPrefix): void;
recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean;
clearTouched(): void;
// -- Low-Level
delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise<void>
trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise<void>
// -- Low-Level
delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise<void>;
trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise<void>;
getFilesIncludeHidden(
basePath: string,
@@ -45,4 +52,4 @@ export interface StorageAccess {
excludeFilter?: RegExp[],
skipFolder?: string[]
): Promise<FilePath[]>;
}
}

View File

@@ -0,0 +1,197 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, VER, type ObsidianLiveSyncSettings } from "../../lib/src/common/types.ts";
import {
EVENT_LAYOUT_READY,
EVENT_PLUGIN_LOADED,
EVENT_PLUGIN_UNLOADED,
EVENT_REQUEST_RELOAD_SETTING_TAB,
EVENT_SETTING_SAVED,
eventHub,
} from "../../common/events.ts";
import { $f, setLang } from "../../lib/src/common/i18n.ts";
import { versionNumberString2Number } from "../../lib/src/string_and_binary/convert.ts";
import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor";
import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule {
async $$onLiveSyncReady() {
if (!(await this.core.$everyOnLayoutReady())) return;
eventHub.emitEvent(EVENT_LAYOUT_READY);
if (this.settings.suspendFileWatching || this.settings.suspendParseReplicationResult) {
const ANSWER_KEEP = "Keep this plug-in suspended";
const ANSWER_RESUME = "Resume and restart Obsidian";
const message = `Self-hosted LiveSync has been configured to ignore some events. Is this intentional for you?
| Type | Status | Note |
|:---:|:---:|---|
| Storage Events | ${this.settings.suspendFileWatching ? "suspended" : "active"} | Every modification will be ignored |
| Database Events | ${this.settings.suspendParseReplicationResult ? "suspended" : "active"} | Every synchronised change will be postponed |
Do you want to resume them and restart Obsidian?
> [!DETAILS]-
> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.
> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.
`;
if (
(await this.core.confirm.askSelectStringDialogue(message, [ANSWER_KEEP, ANSWER_RESUME], {
defaultAction: ANSWER_KEEP,
title: "Scram Enabled",
})) == ANSWER_RESUME
) {
this.settings.suspendFileWatching = false;
this.settings.suspendParseReplicationResult = false;
await this.saveSettings();
await this.core.$$scheduleAppReload();
return;
}
}
const isInitialized = await this.core.$$initializeDatabase(false, false);
if (!isInitialized) {
//TODO:stop all sync.
return false;
}
if (!(await this.core.$everyOnFirstInitialize())) return;
await this.core.$$realizeSettingSyncMode();
fireAndForget(async () => {
this._log(`Additional safety scan..`, LOG_LEVEL_VERBOSE);
if (!(await this.core.$allScanStat())) {
this._log(`Additional safety scan has been failed on some module`, LOG_LEVEL_NOTICE);
} else {
this._log(`Additional safety scan done`, LOG_LEVEL_VERBOSE);
}
});
}
$$wireUpEvents(): void {
eventHub.onEvent(EVENT_SETTING_SAVED, (settings: ObsidianLiveSyncSettings) => {
this.localDatabase.settings = settings;
setLang(settings.displayLanguage);
eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
});
eventHub.onEvent(EVENT_SETTING_SAVED, (settings: ObsidianLiveSyncSettings) => {
fireAndForget(() => this.core.$$realizeSettingSyncMode());
});
}
async $$onLiveSyncLoad(): Promise<void> {
this.$$wireUpEvents();
// debugger;
eventHub.emitEvent(EVENT_PLUGIN_LOADED, this.core);
this._log("loading plugin");
if (!(await this.core.$everyOnloadStart())) {
this._log("Plugin initialising has been cancelled by some module", LOG_LEVEL_NOTICE);
return;
}
// this.addUIs();
//@ts-ignore
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
//@ts-ignore
const packageVersion: string = PACKAGE_VERSION || "0.0.0";
this._log($f`Self-hosted LiveSync${" v"}${manifestVersion} ${packageVersion}`);
await this.core.$$loadSettings();
if (!(await this.core.$everyOnloadAfterLoadSettings())) {
this._log("Plugin initialising has been cancelled by some module", LOG_LEVEL_NOTICE);
return;
}
const lsKey = "obsidian-live-sync-ver" + this.core.$$getVaultName();
const last_version = localStorage.getItem(lsKey);
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
if (lastVersion > this.settings.lastReadUpdates && this.settings.isConfigured) {
this._log($f`You have some unread release notes! Please read them once!`, LOG_LEVEL_NOTICE);
}
//@ts-ignore
if (this.isMobile) {
this.settings.disableRequestURI = true;
}
if (last_version && Number(last_version) < VER) {
this.settings.liveSync = false;
this.settings.syncOnSave = false;
this.settings.syncOnEditorSave = false;
this.settings.syncOnStart = false;
this.settings.syncOnFileOpen = false;
this.settings.syncAfterMerge = false;
this.settings.periodicReplication = false;
this.settings.versionUpFlash = $f`Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.`;
await this.saveSettings();
}
localStorage.setItem(lsKey, `${VER}`);
await this.core.$$openDatabase();
this.core.$$realizeSettingSyncMode = this.core.$$realizeSettingSyncMode.bind(this);
// this.$$parseReplicationResult = this.$$parseReplicationResult.bind(this);
// this.$$replicate = this.$$replicate.bind(this);
this.core.$$onLiveSyncReady = this.core.$$onLiveSyncReady.bind(this);
await this.core.$everyOnload();
await Promise.all(this.core.addOns.map((e) => e.onload()));
}
async $$onLiveSyncUnload(): Promise<void> {
eventHub.emitEvent(EVENT_PLUGIN_UNLOADED);
await this.core.$allStartOnUnload();
cancelAllPeriodicTask();
cancelAllTasks();
stopAllRunningProcessors();
await this.core.$allOnUnload();
this._unloaded = true;
for (const addOn of this.core.addOns) {
addOn.onunload();
}
if (this.localDatabase != null) {
this.localDatabase.onunload();
if (this.core.replicator) {
this.core.replicator?.closeReplication();
}
await this.localDatabase.close();
}
this._log($f`unloading plugin`);
}
async $$realizeSettingSyncMode(): Promise<void> {
await this.core.$everyBeforeSuspendProcess();
await this.core.$everyBeforeRealizeSetting();
this.localDatabase.refreshSettings();
await this.core.$everyCommitPendingFileEvent();
await this.core.$everyRealizeSettingSyncMode();
// disable all sync temporary.
if (this.core.$$isSuspended()) return;
await this.core.$everyOnResumeProcess();
await this.core.$everyAfterResumeProcess();
await this.core.$everyAfterRealizeSetting();
}
$$isReloadingScheduled(): boolean {
return this.core._totalProcessingCount !== undefined;
}
isReady = false;
$$isReady(): boolean {
return this.isReady;
}
$$markIsReady(): void {
this.isReady = true;
}
$$resetIsReady(): void {
this.isReady = false;
}
_suspended = false;
$$isSuspended(): boolean {
return this._suspended || !this.settings?.isConfigured;
}
$$setSuspended(value: boolean) {
this._suspended = value;
}
_unloaded = false;
$$isUnloaded(): boolean {
return this._unloaded;
}
}

View File

@@ -426,4 +426,14 @@ span.ls-mark-cr::after {
background-color: rgba(var(--background-primary), 0.3);
backdrop-filter: blur(4px);
border-radius: 30%;
}
.sls-dialogue-note-wrapper {
display: flex;
justify-content: flex-end;
align-items: center;
}
.sls-dialogue-note-countdown {
font-size: 0.8em;
}

View File

@@ -1,42 +1,82 @@
## 0.24.0 RC Release Note
## 0.24.0
**Note:** This will be rewritten with the stable release. I confess, before you take the time, this is quite long.
I know that we have been waiting for a long time. It is finally released!
Over the past three years since the inception of the plugin, various features have been implemented to address diverse user needs. This is so honourable and I am grateful for your years of support.
However, However, this process has resulted in a codebase that has become increasingly disorganised, with features becoming entangled.
Over the past three years since the inception of the plugin, various features have been implemented to address diverse user needs. This is truly honourable, and I am grateful for your years of support. However, this process has led to an increasingly disorganised codebase, with features becoming entangled. Consequently, this has led to a situation where bugs can go unnoticed and resolving one issue may inadvertently introduce another.
Consequently, this has led to a situation where bugs can go unnoticed or resolving one issue may inadvertently introduce another.
In 0.24.0, I reorganised the previously jumbled main codebase into clearly defined modules. Although I had assumed that the total size of the code would not increase, I discovered that it has in fact increased. While the complexity is still considerable, the refactoring has improved the clarity of the code's structure. Additionally, while testing the release candidates, we still found many bugs to fix, which helped to make this plug-in robust and stable. Therefore, we are now ready to use the updated plug-in, and in addition to that, proceed to the next step.
In 0.24.0, I reorganised the previously disjointed main codebase into clearly defined modules. Although I anticipated that the overall volume of code would not increase, I discovered that it has, in fact, expanded. While the complexity may still be considerable, the refactoring has enhanced clarity regarding the current structure of the code. (The next focus may involve a review of dependencies).
This is also the first step towards a fully-fledged-fancy LiveSync, not just a plug-in from Obsidian. Of course, it will still be a plug-in primarily and foremost, but this development marks a significant step towards the self-hosting concept.
Throughout this process, a significant number of bugs have been resolved. And it may be worth mentioning that these bugs may had given rise to other bugs. I kindly request that you verify whether your issues have been addressed. At least conflict resolution and related issues have improved significantly.
It is also the first step towards a fully-fledged-fancy LiveSync, not just a plug-in from Obsidian. Of course, it will still be a plug-in as a first class and foremost, but this development marks a significant step towards the self-hosting concept.
This dev release is very close to the beta version that I had previously indicated would not be released. As a result, I have faced challenges in maintaining the main branch while working on this dev release. Regrettably, I have not been able to make any commits to the main branch in the last three weeks. Thus, the dev branch will remain reserved for major changes only.
The Release Candidate will be available for a few days and will only be officially released once users, including myself, have confirmed that there are no issues.
Finally, I would like to once again express my respect and gratitude to all of you once again. Thank you for your interest in the development version. Your contributions and dedication are greatly appreciated through testing.
Finally, I would like to once again express my respect and gratitude to all of you. My gratitude extends to all of the dev testers! Your contributions have certainly made the plug-in robust and stable!
Thank you, and I hope your troubles will be resolved!
---
## 0.24.0.dev-rc2
## 0.24.1
#### Fixed
- Vault History can show the correct information of match-or-not for each file and database even if it is a binary file.
- `Sync settings via markdown` is now hidden during the setup wizard.
- Verify and Fix will ignore the hidden files if the hidden file sync is disabled.
#### New feature
- Now we can fetch the tweaks from the remote database while the setting dialogue and wizard are processing.
#### Improved
- More things are moved to the modules.
- Includes the Main codebase. Now `main.ts` is almost stub.
- EventHub is now more robust and typesafe.
## 0.24.0
### Improved
- The welcome message is now more simple to encourage the use of the Setup-URI.
- The secondary message is also simpler to guide users to Minimal Setup.
- But Setup-URI will be recommended again, due to its importance.
- These dialogues contain a link to the documentation which can be clicked.
- The minimal setup is more minimal now. And, the setup is more user-friendly.
- Now the Configuration of the remote database is checked more robustly, but we can ignore the warning and proceed with the setup.
- Before we are asked about each feature, we are asked if we want to use optional features in the first place.
- This is to prevent the user from being overwhelmed by the features.
- And made it clear that it is not recommended for new users.
- Many messages have been improved for better understanding.
- Ridiculous messages have been (carefully) refined.
- Dialogues are more informative and friendly.
- A lot of messages have been mostly rewritten, leveraging Markdown.
- Especially auto-closing dialogues are now explicitly labelled: `To stop the countdown, tap anywhere on the dialogue`.
- Now if the is plugin configured to ignore some events, we will get a chance to fix it, in addition to the warning.
- And why that has happened is also explained in the dialogue.
- A note relating to device names has been added to Customisation Sync on the setting dialogue.
- We can verify and resolve also the hidden files now.
### Fixed
- Some status icons is now shown correctly.
- We can resolve the conflict of the JSON file correctly now.
- Verifying files between the local database and storage is now working correctly.
- While restarting the plug-in, the shown dialogues will be automatically closed to avoid unexpected behaviour.
- Replicated documents that the local device has configured to ignore are now correctly ignored.
- The chunks of the document on the local device during the first transfer will be created correctly.
- And why we should create them is now explained in the dialogue.
- If optional features have been enabled in the wizard, `Enable advanced features` will be toggled correctly.
The hidden file sync is now working correctly. - Now the deletion of hidden files is correctly synchronised.
- Customisation Sync is now working correctly together with hidden file sync.
- No longer database suffix is stored in the setting sharing markdown.
- A fair number of bugs have been fixed.
## 0.24.0-rc1
### Changed
### Fixed
- A fair numbers of bugs have been fixed.
- Some default settings have been changed for an easier new user experience.
- Preventing the meaningless migration of the settings.
### Tiding
- The codebase has been reorganised into clearly defined modules.
- Commented-out codes have been gradually removed.
Older notes is in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
Older notes are in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).

3
utils/flyio/deno.jsonc Normal file
View File

@@ -0,0 +1,3 @@
{
"nodeModulesDir": "none"
}

25
utils/flyio/deno.lock generated Normal file
View File

@@ -0,0 +1,25 @@
{
"version": "4",
"specifiers": {
"npm:octagonal-wheels@0.1.11": "0.1.11"
},
"npm": {
"idb@8.0.0": {
"integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw=="
},
"octagonal-wheels@0.1.11": {
"integrity": "sha512-KsXfpziFHmlLEBe5VAXFz9OyyjJEEdSg7xxASqdzmbe5oo9dhcOeWGrQfyipRJwHAhlFkI4vEf8JCSgkcyRxYg==",
"dependencies": [
"idb",
"xxhash-wasm@0.4.2",
"xxhash-wasm-102@npm:xxhash-wasm@1.0.2"
]
},
"xxhash-wasm@0.4.2": {
"integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA=="
},
"xxhash-wasm@1.0.2": {
"integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A=="
}
}
}

View File

@@ -1,42 +1,175 @@
import { encrypt } from "npm:octagonal-wheels@0.1.11/encryption/encryption.js";
const noun = ["waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning", "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter", "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook", "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly", "feather", "grass", "haze", "mountain", "night", "pond", "darkness", "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder", "violet", "water", "wildflower", "wave", "water", "resonance", "sun", "log", "dream", "cherry", "tree", "fog", "frost", "voice", "paper", "frog", "smoke", "star"];
const adjectives = ["autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark", "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter", "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue", "billowing", "broken", "cold", "damp", "falling", "frosty", "green", "long", "late", "lingering", "bold", "little", "morning", "muddy", "old", "red", "rough", "still", "small", "sparkling", "thrumming", "shy", "wandering", "withered", "wild", "black", "young", "holy", "solitary", "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine", "polished", "ancient", "purple", "lively", "nameless"];
const noun = [
"waterfall",
"river",
"breeze",
"moon",
"rain",
"wind",
"sea",
"morning",
"snow",
"lake",
"sunset",
"pine",
"shadow",
"leaf",
"dawn",
"glitter",
"forest",
"hill",
"cloud",
"meadow",
"sun",
"glade",
"bird",
"brook",
"butterfly",
"bush",
"dew",
"dust",
"field",
"fire",
"flower",
"firefly",
"feather",
"grass",
"haze",
"mountain",
"night",
"pond",
"darkness",
"snowflake",
"silence",
"sound",
"sky",
"shape",
"surf",
"thunder",
"violet",
"water",
"wildflower",
"wave",
"water",
"resonance",
"sun",
"log",
"dream",
"cherry",
"tree",
"fog",
"frost",
"voice",
"paper",
"frog",
"smoke",
"star",
];
const adjectives = [
"autumn",
"hidden",
"bitter",
"misty",
"silent",
"empty",
"dry",
"dark",
"summer",
"icy",
"delicate",
"quiet",
"white",
"cool",
"spring",
"winter",
"patient",
"twilight",
"dawn",
"crimson",
"wispy",
"weathered",
"blue",
"billowing",
"broken",
"cold",
"damp",
"falling",
"frosty",
"green",
"long",
"late",
"lingering",
"bold",
"little",
"morning",
"muddy",
"old",
"red",
"rough",
"still",
"small",
"sparkling",
"thrumming",
"shy",
"wandering",
"withered",
"wild",
"black",
"young",
"holy",
"solitary",
"fragrant",
"aged",
"snowy",
"proud",
"floral",
"restless",
"divine",
"polished",
"ancient",
"purple",
"lively",
"nameless",
];
function friendlyString() {
return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${noun[Math.floor(Math.random() * noun.length)]}`;
}
const uri_passphrase = `${Deno.env.get("uri_passphrase") ?? friendlyString()}`;
const URIBASE = "obsidian://setuplivesync?settings=";
async function main() {
const conf = {
"couchDB_URI": `${Deno.env.get("hostname")}`,
"couchDB_USER": `${Deno.env.get("username")}`,
"couchDB_PASSWORD": `${Deno.env.get("password")}`,
"couchDB_DBNAME": `${Deno.env.get("database")}`,
"syncOnStart": true,
"gcDelay": 0,
"periodicReplication": true,
"syncOnFileOpen": true,
"encrypt": true,
"passphrase": `${Deno.env.get("passphrase")}`,
"usePathObfuscation": true,
"batchSave": true,
"batch_size": 50,
"batches_limit": 50,
"useHistory": true,
"disableRequestURI": true,
"customChunkSize": 50,
"syncAfterMerge": false,
"concurrencyOfReadChunksOnline": 100,
"minimumIntervalOfReadChunksOnline": 100,
}
couchDB_URI: `${Deno.env.get("hostname")}`,
couchDB_USER: `${Deno.env.get("username")}`,
couchDB_PASSWORD: `${Deno.env.get("password")}`,
couchDB_DBNAME: `${Deno.env.get("database")}`,
syncOnStart: true,
gcDelay: 0,
periodicReplication: true,
syncOnFileOpen: true,
encrypt: true,
passphrase: `${Deno.env.get("passphrase")}`,
usePathObfuscation: true,
batchSave: true,
batch_size: 50,
batches_limit: 50,
useHistory: true,
disableRequestURI: true,
customChunkSize: 50,
syncAfterMerge: false,
concurrencyOfReadChunksOnline: 100,
minimumIntervalOfReadChunksOnline: 100,
handleFilenameCaseSensitive: false,
doNotUseFixedRevisionForChunks: false,
settingVersion: 10,
notifyThresholdOfRemoteStorageSize: 800,
};
const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), uri_passphrase, false));
const theURI = `${URIBASE}${encryptedConf}`;
console.log(theURI);
console.log("\nYour passphrase of Setup-URI is: ", uri_passphrase);
console.log("This passphrase is never shown again, so please note it in a safe place.")
console.log("This passphrase is never shown again, so please note it in a safe place.");
console.log(theURI);
}
await main();
await main();