From 89e23b1bf49c4f6c09b76e21c235e8b6754aae08 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Wed, 16 Oct 2024 12:44:07 +0100 Subject: [PATCH 1/8] Preparing v0.24.0 --- .eslintignore | 6 +- .eslintrc | 29 +- esbuild.config.mjs | 2 +- package-lock.json | 229 +- package.json | 8 +- src/common/events.ts | 14 +- src/common/types.ts | 6 +- src/common/utils.ts | 277 +- src/features/CmdSetupLiveSync.ts | 424 -- .../{ => ConfigSync}/CmdConfigSync.ts | 349 +- .../ConfigSync}/PluginCombo.svelte | 12 +- src/features/ConfigSync/PluginDialogModal.ts | 35 + .../ConfigSync}/PluginPane.svelte | 33 +- .../HiddenFileCommon}/JsonResolveModal.ts | 10 +- .../HiddenFileCommon}/JsonResolvePane.svelte | 9 +- .../{ => HiddenFileSync}/CmdHiddenFileSync.ts | 297 +- src/features/LiveSyncCommands.ts | 29 +- src/lib | 2 +- src/main.ts | 3971 ++--------------- src/modules/AbstractModule.ts | 183 + src/modules/AbstractObsidianModule.ts | 55 + src/modules/ModuleTypes.ts | 40 + src/modules/core/ModuleDatabaseFileAccess.ts | 262 ++ src/modules/core/ModuleFileHandler.ts | 370 ++ .../core/ModuleLocalDatabaseObsidian.ts | 23 + src/modules/core/ModulePeriodicProcess.ts | 35 + src/modules/core/ModulePouchDB.ts | 16 + src/modules/core/ModuleRebuilder.ts | 216 + src/modules/core/ModuleReplicator.ts | 343 ++ src/modules/core/ModuleReplicatorCouchDB.ts | 33 + src/modules/core/ModuleReplicatorMinIO.ts | 17 + src/modules/core/ModuleTargetFilter.ts | 146 + .../coreFeatures/ModuleCheckRemoteSize.ts | 90 + .../coreFeatures/ModuleConflictChecker.ts | 75 + .../coreFeatures/ModuleConflictResolver.ts | 141 + src/modules/coreFeatures/ModuleRedFlag.ts | 106 + .../coreFeatures/ModuleRemoteGovernor.ts | 16 + .../ModuleResolveMismatchedTweaks.ts | 97 + .../coreObsidian/ModuleFileAccessObsidian.ts | 330 ++ .../coreObsidian/ModuleInputUIObsidian.ts | 71 + .../coreObsidian/UILib}/dialogs.ts | 94 +- .../storageLib}/SerializedFileAccess.ts | 39 +- .../storageLib/StorageEventManager.ts | 439 ++ .../coreObsidian/storageLib/utilObsidian.ts | 109 + .../essential/ModuleInitializerFile.ts | 328 ++ src/modules/essential/ModuleKeyValueDB.ts | 90 + src/modules/essential/ModuleMigration.ts | 210 + .../APILib}/ObsHttpHandler.ts | 2 +- .../essentialObsidian/ModuleObsidianAPI.ts | 218 + .../essentialObsidian/ModuleObsidianEvents.ts | 217 + .../essentialObsidian/ModuleObsidianMenu.ts | 136 + src/modules/extras/ModuleDev.ts | 111 + src/modules/extras/ModuleIntegratedTest.ts | 441 ++ src/modules/extras/ModuleReplicateTest.ts | 528 +++ src/modules/extras/devUtil/TestPane.svelte | 119 + .../extras/devUtil}/TestPaneView.ts | 12 +- .../extras/devUtil}/testUtils.ts | 11 +- .../extras/devUtil}/tests.ts | 13 +- .../DocumentHistory}/DocumentHistoryModal.ts | 72 +- .../GlobalHistory}/GlobalHistory.svelte | 29 +- .../GlobalHistory}/GlobalHistoryView.ts | 8 +- .../ConflictResolveModal.ts | 22 +- .../features/Log}/LogPane.svelte | 6 +- .../features/Log}/LogPaneView.ts | 6 +- src/modules/features/ModuleGlobalHistory.ts | 29 + .../ModuleInteractiveConflictResolver.ts | 156 + .../features/ModuleLog.ts} | 240 +- .../features/ModuleObsidianDocumentHistory.ts | 56 + src/modules/features/ModuleObsidianSetting.ts | 207 + .../ModuleObsidianSettingAsMarkdown.ts | 211 + .../features/ModuleObsidianSettingTab.ts | 30 + src/modules/features/ModuleSetupObsidian.ts | 205 + .../SettingDialogue}/LiveSyncSetting.ts | 14 +- .../MultipleRegExpControl.svelte | 27 +- .../ObsidianLiveSyncSettingTab.ts | 1869 ++++---- .../SettingDialogue}/settingConstants.ts | 8 +- src/modules/interfaces/Confirm.ts | 14 + src/modules/interfaces/DatabaseFileAccess.ts | 18 + src/modules/interfaces/DatabaseRebuilder.ts | 11 + src/modules/interfaces/StorageAccess.ts | 48 + src/modules/modules.md | 45 + src/storages/StorageEventManager.ts | 307 -- src/tests/TestPane.svelte | 50 - terser.config.mjs | 4 +- tsconfig.json | 28 +- 85 files changed, 9211 insertions(+), 6033 deletions(-) delete mode 100644 src/features/CmdSetupLiveSync.ts rename src/features/{ => ConfigSync}/CmdConfigSync.ts (81%) rename src/{ui/components => features/ConfigSync}/PluginCombo.svelte (96%) create mode 100644 src/features/ConfigSync/PluginDialogModal.ts rename src/{ui => features/ConfigSync}/PluginPane.svelte (93%) rename src/{ui => features/HiddenFileCommon}/JsonResolveModal.ts (87%) rename src/{ui => features/HiddenFileCommon}/JsonResolvePane.svelte (95%) rename src/features/{ => HiddenFileSync}/CmdHiddenFileSync.ts (72%) create mode 100644 src/modules/AbstractModule.ts create mode 100644 src/modules/AbstractObsidianModule.ts create mode 100644 src/modules/ModuleTypes.ts create mode 100644 src/modules/core/ModuleDatabaseFileAccess.ts create mode 100644 src/modules/core/ModuleFileHandler.ts create mode 100644 src/modules/core/ModuleLocalDatabaseObsidian.ts create mode 100644 src/modules/core/ModulePeriodicProcess.ts create mode 100644 src/modules/core/ModulePouchDB.ts create mode 100644 src/modules/core/ModuleRebuilder.ts create mode 100644 src/modules/core/ModuleReplicator.ts create mode 100644 src/modules/core/ModuleReplicatorCouchDB.ts create mode 100644 src/modules/core/ModuleReplicatorMinIO.ts create mode 100644 src/modules/core/ModuleTargetFilter.ts create mode 100644 src/modules/coreFeatures/ModuleCheckRemoteSize.ts create mode 100644 src/modules/coreFeatures/ModuleConflictChecker.ts create mode 100644 src/modules/coreFeatures/ModuleConflictResolver.ts create mode 100644 src/modules/coreFeatures/ModuleRedFlag.ts create mode 100644 src/modules/coreFeatures/ModuleRemoteGovernor.ts create mode 100644 src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts create mode 100644 src/modules/coreObsidian/ModuleFileAccessObsidian.ts create mode 100644 src/modules/coreObsidian/ModuleInputUIObsidian.ts rename src/{common => modules/coreObsidian/UILib}/dialogs.ts (71%) rename src/{storages => modules/coreObsidian/storageLib}/SerializedFileAccess.ts (85%) create mode 100644 src/modules/coreObsidian/storageLib/StorageEventManager.ts create mode 100644 src/modules/coreObsidian/storageLib/utilObsidian.ts create mode 100644 src/modules/essential/ModuleInitializerFile.ts create mode 100644 src/modules/essential/ModuleKeyValueDB.ts create mode 100644 src/modules/essential/ModuleMigration.ts rename src/{common => modules/essentialObsidian/APILib}/ObsHttpHandler.ts (98%) create mode 100644 src/modules/essentialObsidian/ModuleObsidianAPI.ts create mode 100644 src/modules/essentialObsidian/ModuleObsidianEvents.ts create mode 100644 src/modules/essentialObsidian/ModuleObsidianMenu.ts create mode 100644 src/modules/extras/ModuleDev.ts create mode 100644 src/modules/extras/ModuleIntegratedTest.ts create mode 100644 src/modules/extras/ModuleReplicateTest.ts create mode 100644 src/modules/extras/devUtil/TestPane.svelte rename src/{tests => modules/extras/devUtil}/TestPaneView.ts (75%) rename src/{tests => modules/extras/devUtil}/testUtils.ts (80%) rename src/{tests => modules/extras/devUtil}/tests.ts (83%) rename src/{ui => modules/features/DocumentHistory}/DocumentHistoryModal.ts (82%) rename src/{ui => modules/features/GlobalHistory}/GlobalHistory.svelte (91%) rename src/{ui => modules/features/GlobalHistory}/GlobalHistoryView.ts (84%) rename src/{ui => modules/features/InteractiveConflictResolving}/ConflictResolveModal.ts (88%) rename src/{ui => modules/features/Log}/LogPane.svelte (90%) rename src/{ui => modules/features/Log}/LogPaneView.ts (85%) create mode 100644 src/modules/features/ModuleGlobalHistory.ts create mode 100644 src/modules/features/ModuleInteractiveConflictResolver.ts rename src/{features/CmdStatusInsideEditor.ts => modules/features/ModuleLog.ts} (53%) create mode 100644 src/modules/features/ModuleObsidianDocumentHistory.ts create mode 100644 src/modules/features/ModuleObsidianSetting.ts create mode 100644 src/modules/features/ModuleObsidianSettingAsMarkdown.ts create mode 100644 src/modules/features/ModuleObsidianSettingTab.ts create mode 100644 src/modules/features/ModuleSetupObsidian.ts rename src/{ui/components => modules/features/SettingDialogue}/LiveSyncSetting.ts (96%) rename src/{ui/components => modules/features/SettingDialogue}/MultipleRegExpControl.svelte (67%) rename src/{ui => modules/features/SettingDialogue}/ObsidianLiveSyncSettingTab.ts (55%) rename src/{ui => modules/features/SettingDialogue}/settingConstants.ts (98%) create mode 100644 src/modules/interfaces/Confirm.ts create mode 100644 src/modules/interfaces/DatabaseFileAccess.ts create mode 100644 src/modules/interfaces/DatabaseRebuilder.ts create mode 100644 src/modules/interfaces/StorageAccess.ts create mode 100644 src/modules/modules.md delete mode 100644 src/storages/StorageEventManager.ts delete mode 100644 src/tests/TestPane.svelte diff --git a/.eslintignore b/.eslintignore index 7b703be..a6115c3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,8 @@ node_modules build .eslintrc.js.bak src/lib/src/patches/pouchdb-utils -esbuild.config.mjs \ No newline at end of file +esbuild.config.mjs +rollup.config.js +src/lib/test +src/lib/src/cli +main.js diff --git a/.eslintrc b/.eslintrc index 08e619d..f9ca43f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,20 +1,13 @@ { "root": true, "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"], "parserOptions": { "sourceType": "module", - "project": [ - "tsconfig.json" - ] + "project": ["tsconfig.json"] }, + "ignorePatterns": ["src/lib/src/API/*.ts"], "rules": { "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ @@ -28,9 +21,17 @@ "no-prototype-builtins": "off", "@typescript-eslint/no-empty-function": "off", "require-await": "warn", - "no-async-promise-executor": "off", + "@typescript-eslint/require-await": "warn", + "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/no-floating-promises": "warn", + "no-async-promise-executor": "warn", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unnecessary-type-assertion": "error", - "no-constant-condition": ["error", { "checkLoops": false }] - } + "no-constant-condition": [ + "error", + { + "checkLoops": false + } + ] } +} diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 1bfcd9e..495c887 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -12,7 +12,7 @@ import inlineWorkerPlugin from "esbuild-plugin-inline-worker"; import { terserOption } from "./terser.config.mjs"; const prod = process.argv[2] === "production"; -const keepTest = !prod; +const keepTest = true; //!prod; const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + ""); const packageJson = JSON.parse(fs.readFileSync("./package.json") + ""); diff --git a/package-lock.json b/package-lock.json index 919fc63..aa5ee51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.rc1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.rc1", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", @@ -19,6 +19,7 @@ "idb": "^8.0.0", "minimatch": "^10.0.1", "octagonal-wheels": "^0.1.15", + "svelte-check": "^4.0.4", "xxhash-wasm": "0.4.2", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" }, @@ -78,7 +79,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -1578,7 +1578,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1592,7 +1591,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -1601,7 +1599,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -1619,14 +1616,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2408,8 +2403,7 @@ "node_modules/@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -2848,7 +2842,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2915,7 +2908,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -3055,7 +3047,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", - "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -3154,11 +3145,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/code-red": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.3.tgz", "integrity": "sha512-kVwJELqiILQyG5aeuyKFbdsI1fmQy1Cmf7dQ8eGmVuJoaRVdwey7WaMknr2ZFeVSYSKT0rExsa8EGw0aoI/1QQ==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", "@types/estree": "^1.0.0", @@ -3226,7 +3230,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" @@ -3350,7 +3353,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -3916,7 +3918,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -4622,7 +4623,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz", "integrity": "sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==", - "dev": true, "dependencies": { "@types/estree": "*" } @@ -4791,8 +4791,7 @@ "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -4819,7 +4818,6 @@ "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -4852,8 +4850,7 @@ "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, "node_modules/merge2": { "version": "1.4.1", @@ -4931,6 +4928,14 @@ "node": "*" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5220,7 +5225,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", @@ -5230,8 +5234,7 @@ "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5728,6 +5731,18 @@ } ] }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -5826,6 +5841,17 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -5964,7 +5990,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6111,7 +6136,6 @@ "version": "4.2.19", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -6133,6 +6157,54 @@ "node": ">=16" } }, + "node_modules/svelte-check": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.5.tgz", + "integrity": "sha512-icBTBZ3ibBaywbXUat3cK6hB5Du+Kq9Z8CRuyLmm64XIe2/r+lQcbuBx/IQgsbrC+kT2jQ0weVpZSSRIPwB6jQ==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-check/node_modules/fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/svelte-preprocess": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.2.tgz", @@ -6389,7 +6461,6 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6591,7 +6662,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -7624,7 +7694,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -7634,14 +7703,12 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, "@jridgewell/source-map": { "version": "0.3.5", @@ -7656,14 +7723,12 @@ "@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -8267,8 +8332,7 @@ "@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" }, "@types/json5": { "version": "0.0.29", @@ -8605,8 +8669,7 @@ "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" }, "acorn-jsx": { "version": "5.3.2", @@ -8652,7 +8715,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, "requires": { "dequal": "^2.0.3" } @@ -8748,7 +8810,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", - "dev": true, "requires": { "dequal": "^2.0.3" } @@ -8821,11 +8882,18 @@ "supports-color": "^7.1.0" } }, + "chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "requires": { + "readdirp": "^4.0.1" + } + }, "code-red": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.3.tgz", "integrity": "sha512-kVwJELqiILQyG5aeuyKFbdsI1fmQy1Cmf7dQ8eGmVuJoaRVdwey7WaMknr2ZFeVSYSKT0rExsa8EGw0aoI/1QQ==", - "dev": true, "requires": { "@jridgewell/sourcemap-codec": "^1.4.14", "@types/estree": "^1.0.0", @@ -8887,7 +8955,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, "requires": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" @@ -8966,8 +9033,7 @@ "dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, "detect-libc": { "version": "1.0.3", @@ -9408,7 +9474,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, "requires": { "@types/estree": "^1.0.0" } @@ -9906,7 +9971,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz", "integrity": "sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==", - "dev": true, "requires": { "@types/estree": "*" } @@ -10027,8 +10091,7 @@ "locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, "locate-path": { "version": "6.0.0", @@ -10049,7 +10112,6 @@ "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "dev": true, "requires": { "@jridgewell/sourcemap-codec": "^1.4.15" } @@ -10072,8 +10134,7 @@ "mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, "merge2": { "version": "1.4.1", @@ -10126,6 +10187,11 @@ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "dev": true }, + "mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -10324,7 +10390,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, "requires": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", @@ -10334,8 +10399,7 @@ "picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "picomatch": { "version": "2.3.1", @@ -10747,6 +10811,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==" + }, "regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -10806,6 +10875,14 @@ "queue-microtask": "^1.2.2" } }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "requires": { + "mri": "^1.1.0" + } + }, "safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -10903,8 +10980,7 @@ "source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" }, "source-map-support": { "version": "0.5.21", @@ -11008,7 +11084,6 @@ "version": "4.2.19", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "dev": true, "requires": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -11026,6 +11101,33 @@ "periscopic": "^3.1.0" } }, + "svelte-check": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.5.tgz", + "integrity": "sha512-icBTBZ3ibBaywbXUat3cK6hB5Du+Kq9Z8CRuyLmm64XIe2/r+lQcbuBx/IQgsbrC+kT2jQ0weVpZSSRIPwB6jQ==", + "requires": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "dependencies": { + "fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "requires": {} + }, + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "optional": true, + "peer": true + } + } + }, "svelte-preprocess": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.2.tgz", @@ -11181,8 +11283,7 @@ "typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==" }, "unbox-primitive": { "version": "1.0.2", diff --git a/package.json b/package.json index 257daad..4e3b261 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.23.23", + "version": "0.24.0.rc1", "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", @@ -8,7 +8,10 @@ "dev": "node esbuild.config.mjs", "build": "node esbuild.config.mjs production", "buildDev": "node esbuild.config.mjs dev", - "lint": "eslint src" + "lint": "eslint src", + "svelte-check": "svelte-check --tsconfig ./tsconfig.json", + "tsc-check": "tsc --noEmit", + "check": "npm run lint && npm run svelte-check && npm run tsc-check" }, "keywords": [], "author": "vorotamoroz", @@ -66,6 +69,7 @@ "idb": "^8.0.0", "minimatch": "^10.0.1", "octagonal-wheels": "^0.1.15", + "svelte-check": "^4.0.4", "xxhash-wasm": "0.4.2", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" } diff --git a/src/common/events.ts b/src/common/events.ts index 15baf5d..24930b7 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -3,9 +3,21 @@ export const EVENT_PLUGIN_LOADED = "plugin-loaded"; export const EVENT_PLUGIN_UNLOADED = "plugin-unloaded"; export const EVENT_SETTING_SAVED = "setting-saved"; export const EVENT_FILE_RENAMED = "file-renamed"; - +export const EVENT_FILE_SAVED = "file-saved"; 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_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"; diff --git a/src/common/types.ts b/src/common/types.ts index 2d0f118..c2aacf2 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,5 +1,5 @@ import { type PluginManifest, TFile } from "../deps.ts"; -import { type DatabaseEntry, type EntryBody, type FilePath } 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; @@ -49,9 +49,9 @@ export type queueItem = { }; export type CacheData = string | ArrayBuffer; -export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL"; +export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "INTERNAL"; export type FileEventArgs = { - file: FileInfo | InternalFileInfo; + file: UXFileInfoStub | UXInternalFileInfoStub; cache?: CacheData; oldPath?: string; ctx?: any; diff --git a/src/common/utils.ts b/src/common/utils.ts index 38cc920..7e25921 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,10 +1,9 @@ -import { normalizePath, Platform, TAbstractFile, App, type RequestUrlParam, requestUrl, TFile } from "../deps.ts"; +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 { Logger } from "../lib/src/common/logger.ts"; -import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } 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 { InputStringDialog, PopoverSelectString } from "./dialogs.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"; @@ -46,6 +45,10 @@ export function getPathWithoutPrefix(entry: AnyEntry) { export function getPathFromTFile(file: TAbstractFile) { return file.path as FilePath; } +export function getPathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix) { + if (typeof file == "string") return file as FilePathWithPrefix; + return file.path; +} const memos: { [key: string]: any } = {}; @@ -72,206 +75,6 @@ export function disposeMemoObject(key: string) { delete memos[key]; } -export function isSensibleMargeApplicable(path: string) { - if (path.endsWith(".md")) return true; - return false; -} -export function isObjectMargeApplicable(path: string) { - if (path.endsWith(".canvas")) return true; - if (path.endsWith(".json")) return true; - return false; -} - -export function tryParseJSON(str: string, fallbackValue?: any) { - try { - return JSON.parse(str); - } catch (ex) { - return fallbackValue; - } -} - -const MARK_OPERATOR = `\u{0001}`; -const MARK_DELETED = `${MARK_OPERATOR}__DELETED`; -const MARK_ISARRAY = `${MARK_OPERATOR}__ARRAY`; -const MARK_SWAPPED = `${MARK_OPERATOR}__SWAP`; - -function unorderedArrayToObject(obj: Array) { - return obj.map(e => ({ [e.id as string]: e })).reduce((p, c) => ({ ...p, ...c }), {}) -} -function objectToUnorderedArray(obj: object) { - const entries = Object.entries(obj); - if (entries.some(e => e[0] != e[1]?.id)) throw new Error("Item looks like not unordered array") - return entries.map(e => e[1]); -} -function generatePatchUnorderedArray(from: Array, to: Array) { - if (from.every(e => typeof (e) == "object" && ("id" in e)) && to.every(e => typeof (e) == "object" && ("id" in e))) { - const fObj = unorderedArrayToObject(from); - const tObj = unorderedArrayToObject(to); - const diff = generatePatchObj(fObj, tObj); - if (Object.keys(diff).length > 0) { - return { [MARK_ISARRAY]: diff }; - } else { - return {}; - } - } - return { [MARK_SWAPPED]: to }; -} - -export function generatePatchObj(from: Record, to: Record) { - const entries = Object.entries(from); - const tempMap = new Map(entries); - const ret = {} as Record; - const newEntries = Object.entries(to); - for (const [key, value] of newEntries) { - if (!tempMap.has(key)) { - //New - ret[key] = value; - tempMap.delete(key); - } else { - //Exists - const v = tempMap.get(key); - if (typeof (v) !== typeof (value) || (Array.isArray(v) !== Array.isArray(value))) { - //if type is not match, replace completely. - ret[key] = { [MARK_SWAPPED]: value }; - } else { - if (v === null && value === null) { - // NO OP. - } else if (v === null && value !== null) { - ret[key] = { [MARK_SWAPPED]: value }; - } else if (v !== null && value === null) { - ret[key] = { [MARK_SWAPPED]: value }; - } else if (typeof (v) == "object" && typeof (value) == "object" && !Array.isArray(v) && !Array.isArray(value)) { - const wk = generatePatchObj(v, value); - if (Object.keys(wk).length > 0) ret[key] = wk; - } else if (typeof (v) == "object" && typeof (value) == "object" && Array.isArray(v) && Array.isArray(value)) { - const wk = generatePatchUnorderedArray(v, value); - if (Object.keys(wk).length > 0) ret[key] = wk; - } else if (typeof (v) != "object" && typeof (value) != "object") { - if (JSON.stringify(tempMap.get(key)) !== JSON.stringify(value)) { - ret[key] = value; - } - } else { - if (JSON.stringify(tempMap.get(key)) !== JSON.stringify(value)) { - ret[key] = { [MARK_SWAPPED]: value }; - } - } - } - tempMap.delete(key); - } - } - //Not used item, means deleted one - for (const [key,] of tempMap) { - ret[key] = MARK_DELETED - } - return ret; -} - - -export function applyPatch(from: Record, patch: Record) { - const ret = from; - const patches = Object.entries(patch); - for (const [key, value] of patches) { - if (value == MARK_DELETED) { - delete ret[key]; - continue; - } - if (value === null) { - ret[key] = null; - continue; - } - if (typeof (value) == "object") { - if (MARK_SWAPPED in value) { - ret[key] = value[MARK_SWAPPED]; - continue; - } - if (MARK_ISARRAY in value) { - if (!(key in ret)) ret[key] = []; - if (!Array.isArray(ret[key])) { - throw new Error("Patch target type is mismatched (array to something)"); - } - const orgArrayObject = unorderedArrayToObject(ret[key]); - const appliedObject = applyPatch(orgArrayObject, value[MARK_ISARRAY]); - const appliedArray = objectToUnorderedArray(appliedObject); - ret[key] = [...appliedArray]; - } else { - if (!(key in ret)) { - ret[key] = value; - continue; - } - ret[key] = applyPatch(ret[key], value); - } - } else { - ret[key] = value; - } - } - return ret; -} - -export function mergeObject( - objA: Record | [any], - objB: Record | [any] -) { - const newEntries = Object.entries(objB); - const ret: any = { ...objA }; - if ( - typeof objA !== typeof objB || - Array.isArray(objA) !== Array.isArray(objB) - ) { - return objB; - } - - for (const [key, v] of newEntries) { - if (key in ret) { - const value = ret[key]; - if ( - typeof v !== typeof value || - Array.isArray(v) !== Array.isArray(value) - ) { - //if type is not match, replace completely. - ret[key] = v; - } else { - if ( - typeof v == "object" && - typeof value == "object" && - !Array.isArray(v) && - !Array.isArray(value) - ) { - ret[key] = mergeObject(v, value); - } else if ( - typeof v == "object" && - typeof value == "object" && - Array.isArray(v) && - Array.isArray(value) - ) { - ret[key] = [...new Set([...v, ...value])]; - } else { - ret[key] = v; - } - } - } else { - ret[key] = v; - } - } - const retSorted = Object.fromEntries(Object.entries(ret).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)); - if (Array.isArray(objA) && Array.isArray(objB)) { - return Object.values(retSorted); - } - return retSorted; -} - -export function flattenObject(obj: Record, path: string[] = []): [string, any][] { - if (typeof (obj) != "object") return [[path.join("."), obj]]; - if (obj === null) return [[path.join("."), null]]; - if (Array.isArray(obj)) return [[path.join("."), JSON.stringify(obj)]]; - const e = Object.entries(obj); - const ret = [] - for (const [key, value] of e) { - const p = flattenObject(value, [...path, key]); - ret.push(...p); - } - return ret; -} - export function isValidPath(filename: string) { if (Platform.isDesktop) { @@ -319,29 +122,6 @@ export function isCustomisationSyncMetadata(str: string): boolean { return str.startsWith(ICXHeader); } -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")); - popover.open(); - }); -}; - -export const askSelectString = (app: App, message: string, items: string[]): Promise => { - const getItemsFun = () => items; - return new Promise((res) => { - const popover = new PopoverSelectString(app, message, "", getItemsFun, (result) => res(result)); - popover.open(); - }); -}; - - -export const askString = (app: App, title: string, key: string, placeholder: string, isPassword: boolean = false): Promise => { - return new Promise((res) => { - const dialog = new InputStringDialog(app, title, key, placeholder, isPassword, (result) => res(result)); - dialog.open(); - }); -}; - export class PeriodicProcessor { _process: () => Promise; @@ -413,20 +193,6 @@ export const requestToCouchDB = async (baseUri: string, username: string, passwo return await _requestToCouchDB(baseUri, username, password, origin, uri, body, method); }; -export async function performRebuildDB(plugin: ObsidianLiveSyncPlugin, method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks") { - if (method == "localOnly") { - await plugin.addOnSetup.fetchLocal(); - } - if (method == "localOnlyWithChunks") { - await plugin.addOnSetup.fetchLocal(true); - } - if (method == "remoteOnly") { - await plugin.addOnSetup.rebuildRemote(); - } - if (method == "rebuildBothByThisDevice") { - await plugin.addOnSetup.rebuildEverything(); - } -} export const BASE_IS_NEW = Symbol("base"); export const TARGET_IS_NEW = Symbol("target"); @@ -445,9 +211,9 @@ export function compareMTime(baseMTime: number, targetMTime: number): typeof BAS throw new Error("Unexpected error"); } -export function markChangesAreSame(file: TFile | AnyEntry | string, mtime1: number, mtime2: number) { +export function markChangesAreSame(file: AnyEntry | string | UXFileInfoStub, mtime1: number, mtime2: number) { if (mtime1 === mtime2) return true; - const key = typeof file == "string" ? file : file instanceof TFile ? file.path : file.path ?? file._id; + const 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)) { sameChangePairs.set(key, [...new Set([...pairs, mtime1, mtime2])]); @@ -455,20 +221,20 @@ export function markChangesAreSame(file: TFile | AnyEntry | string, mtime1: numb sameChangePairs.set(key, [mtime1, mtime2]); } } -export function isMarkedAsSameChanges(file: TFile | AnyEntry | string, mtimes: number[]) { - const key = typeof file == "string" ? file : file instanceof TFile ? file.path : file.path ?? file._id; +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)) { return EVEN; } } -export function compareFileFreshness(baseFile: TFile | AnyEntry | undefined, checkTarget: TFile | 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 = baseFile instanceof TFile ? baseFile?.stat?.mtime ?? 0 : baseFile?.mtime ?? 0; - const modifiedTarget = checkTarget instanceof TFile ? 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; @@ -484,15 +250,15 @@ const _cached = new Map boolean; + validator?: (context: Map) => boolean; } export function useMemo({ key, forceUpdate, validator }: MemoOption, updateFunc: (context: Map, prev: T) => T): T { const cached = _cached.get(key); - if (cached && !forceUpdate && (!validator || validator && !validator())) { + const context = cached?.context || new Map(); + if (cached && !forceUpdate && (!validator || validator && !validator(context))) { return cached.value; } - const context = cached?.context || new Map(); const value = updateFunc(context, cached?.value); if (value !== cached?.value) { _cached.set(key, { value, context }); @@ -535,4 +301,13 @@ export function disposeMemo(key: string) { export function disposeAllMemo() { _cached.clear(); -} \ No newline at end of file +} + +export function displayRev(rev: string) { + const [number, hash] = rev.split("-"); + return `${number}-${hash.substring(0, 6)}`; +} + +// export function getPathFromUXFileInfo(file: UXFileInfoStub | UXFileInfo | string) { +// return (typeof file == "string" ? file : file.path) as FilePathWithPrefix; +// } \ No newline at end of file diff --git a/src/features/CmdSetupLiveSync.ts b/src/features/CmdSetupLiveSync.ts deleted file mode 100644 index 268a702..0000000 --- a/src/features/CmdSetupLiveSync.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { type EntryDoc, type ObsidianLiveSyncSettings, DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, REMOTE_COUCHDB, REMOTE_MINIO } from "../lib/src/common/types.ts"; -import { configURIBase } from "../common/types.ts"; -import { Logger } from "../lib/src/common/logger.ts"; -import { PouchDB } from "../lib/src/pouchdb/pouchdb-browser.js"; -import { askSelectString, askYesNo, askString } from "../common/utils.ts"; -import { decrypt, encrypt } from "../lib/src/encryption/e2ee_v2.ts"; -import { LiveSyncCommands } from "./LiveSyncCommands.ts"; -import { delay, fireAndForget } from "../lib/src/common/utils.ts"; -import { confirmWithMessage } from "../common/dialogs.ts"; -import { Platform } from "../deps.ts"; -import { fetchAllUsedChunks } from "../lib/src/pouchdb/utils_couchdb.ts"; -import type { LiveSyncCouchDBReplicator } from "../lib/src/replication/couchdb/LiveSyncReplicator.js"; - -export class SetupLiveSync extends LiveSyncCommands { - onunload() { } - onload(): void | Promise { - this.plugin.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => await this.setupWizard(conf.settings)); - - this.plugin.addCommand({ - id: "livesync-copysetupuri", - name: "Copy settings as a new setup URI", - callback: () => fireAndForget(this.command_copySetupURI()), - }); - this.plugin.addCommand({ - id: "livesync-copysetupuri-short", - name: "Copy settings as a new setup URI (With customization sync)", - callback: () => fireAndForget(this.command_copySetupURIWithSync()), - }); - - this.plugin.addCommand({ - id: "livesync-copysetupurifull", - name: "Copy settings as a new setup URI (Full)", - callback: () => fireAndForget(this.command_copySetupURIFull()), - }); - - this.plugin.addCommand({ - id: "livesync-opensetupuri", - name: "Use the copied setup URI (Formerly Open setup URI)", - callback: () => fireAndForget(this.command_openSetupURI()), - }); - } - onInitializeDatabase(showNotice: boolean) { } - beforeReplicate(showNotice: boolean) { } - onResume() { } - parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument): boolean | Promise { - return false; - } - async realizeSettingSyncMode() { } - - async command_copySetupURI(stripExtra = true) { - const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true); - if (encryptingPassphrase === false) - return; - const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" } as Partial; - 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] : "*")) { - delete setting[k]; - } - } - const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false)); - const uri = `${configURIBase}${encryptedSetting}`; - await navigator.clipboard.writeText(uri); - Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); - } - async command_copySetupURIFull() { - const encryptingPassphrase = await askString(this.app, "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); - Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); - } - async command_copySetupURIWithSync() { - await this.command_copySetupURI(false); - } - async command_openSetupURI() { - const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`); - if (setupURI === false) - return; - if (!setupURI.startsWith(`${configURIBase}`)) { - Logger("Set up URI looks wrong.", LOG_LEVEL_NOTICE); - return; - } - const config = decodeURIComponent(setupURI.substring(configURIBase.length)); - console.dir(config); - await this.setupWizard(config); - } - async setupWizard(confString: string) { - try { - const oldConf = JSON.parse(JSON.stringify(this.settings)); - const encryptingPassphrase = await askString(this.app, "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 askYesNo(this.app, "Importing LiveSync's conf, OK?"); - if (result == "yes") { - const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf) as ObsidianLiveSyncSettings; - this.plugin.replicator.closeReplication(); - this.settings.suspendFileWatching = true; - console.dir(newSettingW); - // Back into the default method once. - newSettingW.configPassphraseStore = ""; - newSettingW.encryptedPassphrase = ""; - newSettingW.encryptedCouchDBConnection = ""; - 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"; - const setupAgain = "Reconfigure and reconstitute the data"; - const setupManually = "Leave everything to me"; - newSettingW.syncInternalFiles = false; - newSettingW.usePluginSync = false; - newSettingW.isConfigured = true; - // Migrate completely obsoleted configuration. - if (!newSettingW.useIndexedDBAdapter) { - newSettingW.useIndexedDBAdapter = true; - } - - const setupType = await askSelectString(this.app, "How would you like to set it up?", [setupAsNew, setupAgain, setupAsMerge, setupJustImport, setupManually]); - if (setupType == setupJustImport) { - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.plugin.saveSettings(); - } else if (setupType == setupAsNew) { - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.fetchLocal(); - } else if (setupType == setupAsMerge) { - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.fetchLocalWithRebuild(); - } 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 askSelectString(this.app, "Do you really want to do this?", ["Cancel", confirm]) != confirm) { - return; - } - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.rebuildEverything(); - } else if (setupType == setupManually) { - const keepLocalDB = await askYesNo(this.app, "Keep local DB?"); - const keepRemoteDB = await askYesNo(this.app, "Keep remote DB?"); - if (keepLocalDB == "yes" && keepRemoteDB == "yes") { - // nothing to do. so peaceful. - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - this.suspendAllSync(); - this.suspendExtraSync(); - await this.plugin.saveSettings(); - const replicate = await askYesNo(this.app, "Unlock and replicate?"); - if (replicate == "yes") { - await this.plugin.replicate(true); - await this.plugin.markRemoteUnlocked(); - } - Logger("Configuration loaded.", LOG_LEVEL_NOTICE); - return; - } - if (keepLocalDB == "no" && keepRemoteDB == "no") { - const reset = await askYesNo(this.app, "Drop everything?"); - if (reset != "yes") { - Logger("Cancelled", LOG_LEVEL_NOTICE); - this.plugin.settings = oldConf; - return; - } - } - let initDB; - this.plugin.settings = newSettingW; - this.plugin.usedPassphrase = ""; - await this.plugin.saveSettings(); - if (keepLocalDB == "no") { - await this.plugin.resetLocalDatabase(); - await this.plugin.localDatabase.initializeDatabase(); - const rebuild = await askYesNo(this.app, "Rebuild the database?"); - if (rebuild == "yes") { - initDB = this.plugin.initializeDatabase(true); - } else { - await this.plugin.markRemoteResolved(); - } - } - if (keepRemoteDB == "no") { - await this.plugin.tryResetRemoteDatabase(); - await this.plugin.markRemoteLocked(); - } - if (keepLocalDB == "no" || keepRemoteDB == "no") { - const replicate = await askYesNo(this.app, "Replicate once?"); - if (replicate == "yes") { - if (initDB != null) { - await initDB; - } - await this.plugin.replicate(true); - } - } - } - } - - Logger("Configuration loaded.", LOG_LEVEL_NOTICE); - } else { - Logger("Cancelled.", LOG_LEVEL_NOTICE); - } - } catch (ex) { - Logger("Couldn't parse or decrypt configuration uri.", LOG_LEVEL_NOTICE); - } - } - - suspendExtraSync() { - Logger("Hidden files and plugin synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) - this.plugin.settings.syncInternalFiles = false; - this.plugin.settings.usePluginSync = false; - this.plugin.settings.autoSweepPlugins = false; - } - async askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { - this.plugin.addOnSetup.suspendExtraSync(); - const message = `Would you like to enable \`Hidden File Synchronization\` or \`Customization sync\`? -${opt.enableFetch ? " - Fetch: Use files stored from other devices. \n" : ""}${opt.enableOverwrite ? "- Overwrite: Use files from this device. \n" : ""}- Custom: Synchronize only customization files with a dedicated interface. -- Keep them disabled: Do not use hidden file synchronization. -Of course, we are able to disable these features.` - const CHOICE_FETCH = "Fetch"; - const CHOICE_OVERWRITE = "Overwrite"; - const CHOICE_CUSTOMIZE = "Custom"; - const CHOICE_DISMISS = "keep them disabled"; - const choices = []; - if (opt?.enableFetch) { - choices.push(CHOICE_FETCH); - } - if (opt?.enableOverwrite) { - choices.push(CHOICE_OVERWRITE); - } - choices.push(CHOICE_CUSTOMIZE); - choices.push(CHOICE_DISMISS); - - const ret = await confirmWithMessage(this.plugin, "Hidden file sync", message, choices, CHOICE_DISMISS, 40); - if (ret == CHOICE_FETCH) { - await this.configureHiddenFileSync("FETCH"); - } else if (ret == CHOICE_OVERWRITE) { - await this.configureHiddenFileSync("OVERWRITE"); - } else if (ret == CHOICE_DISMISS) { - await this.configureHiddenFileSync("DISABLE"); - } else if (ret == CHOICE_CUSTOMIZE) { - await this.configureHiddenFileSync("CUSTOMIZE"); - } - } - async configureHiddenFileSync(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "CUSTOMIZE") { - this.plugin.addOnSetup.suspendExtraSync(); - if (mode == "DISABLE") { - this.plugin.settings.syncInternalFiles = false; - this.plugin.settings.usePluginSync = false; - await this.plugin.saveSettings(); - return; - } - if (mode != "CUSTOMIZE") { - Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE); - if (mode == "FETCH") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pullForce", true); - } else if (mode == "OVERWRITE") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pushForce", true); - } else if (mode == "MERGE") { - await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("safe", true); - } - this.plugin.settings.syncInternalFiles = true; - await this.plugin.saveSettings(); - Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE); - } else if (mode == "CUSTOMIZE") { - if (!this.plugin.deviceAndVaultName) { - let name = await askString(this.app, "Device name", "Please set this device name", `desktop`); - if (!name) { - if (Platform.isAndroidApp) { - name = "android-app" - } else if (Platform.isIosApp) { - name = "ios" - } else if (Platform.isMacOS) { - name = "macos" - } else if (Platform.isMobileApp) { - name = "mobile-app" - } else if (Platform.isMobile) { - name = "mobile" - } else if (Platform.isSafari) { - name = "safari" - } else if (Platform.isDesktop) { - name = "desktop" - } else if (Platform.isDesktopApp) { - name = "desktop-app" - } else { - name = "unknown" - } - name = name + Math.random().toString(36).slice(-4); - } - this.plugin.deviceAndVaultName = name; - } - this.plugin.settings.usePluginSync = true; - await this.plugin.saveSettings(); - await this.plugin.addOnConfigSync.scanAllConfigFiles(true); - } - - } - - suspendAllSync() { - this.plugin.settings.liveSync = false; - this.plugin.settings.periodicReplication = false; - this.plugin.settings.syncOnSave = false; - this.plugin.settings.syncOnEditorSave = false; - this.plugin.settings.syncOnStart = false; - this.plugin.settings.syncOnFileOpen = false; - this.plugin.settings.syncAfterMerge = false; - //this.suspendExtraSync(); - } - async suspendReflectingDatabase() { - if (this.plugin.settings.doNotSuspendOnFetching) return; - if (this.plugin.settings.remoteType == REMOTE_MINIO) return; - Logger(`Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`, LOG_LEVEL_NOTICE); - this.plugin.settings.suspendParseReplicationResult = true; - this.plugin.settings.suspendFileWatching = true; - await this.plugin.saveSettings(); - } - async resumeReflectingDatabase() { - if (this.plugin.settings.doNotSuspendOnFetching) return; - if (this.plugin.settings.remoteType == REMOTE_MINIO) return; - Logger(`Database and storage reflection has been resumed!`, LOG_LEVEL_NOTICE); - this.plugin.settings.suspendParseReplicationResult = false; - this.plugin.settings.suspendFileWatching = false; - await this.plugin.syncAllFiles(true); - await this.plugin.loadQueuedFiles(); - await this.plugin.saveSettings(); - - } - async askUseNewAdapter() { - if (!this.plugin.settings.useIndexedDBAdapter) { - const message = `Now this plugin has been configured to use the old database adapter for keeping compatibility. Do you want to deactivate it?`; - const CHOICE_YES = "Yes, disable and use latest"; - const CHOICE_NO = "No, keep compatibility"; - const choices = [CHOICE_YES, CHOICE_NO]; - - const ret = await confirmWithMessage(this.plugin, "Database adapter", message, choices, CHOICE_YES, 10); - if (ret == CHOICE_YES) { - this.plugin.settings.useIndexedDBAdapter = true; - } - } - } - async resetLocalDatabase() { - if (this.plugin.settings.isConfigured && this.plugin.settings.additionalSuffixOfDatabaseName == "") { - // Discard the non-suffixed database - await this.plugin.resetLocalDatabase(); - } - this.plugin.settings.additionalSuffixOfDatabaseName = `${("appId" in this.app ? this.app.appId : "")}` - await this.plugin.resetLocalDatabase(); - } - async fetchRemoteChunks() { - if (!this.plugin.settings.doNotSuspendOnFetching && this.plugin.settings.readChunksOnline && this.plugin.settings.remoteType == REMOTE_COUCHDB) { - Logger(`Fetching chunks`, LOG_LEVEL_NOTICE); - const replicator = this.plugin.getReplicator() as LiveSyncCouchDBReplicator; - const remoteDB = await replicator.connectRemoteCouchDBWithSetting(this.settings, this.plugin.getIsMobile(), true); - if (typeof remoteDB == "string") { - Logger(remoteDB, LOG_LEVEL_NOTICE); - } else { - await fetchAllUsedChunks(this.localDatabase.localDatabase, remoteDB.db); - } - Logger(`Fetching chunks done`, LOG_LEVEL_NOTICE); - } - } - async fetchLocal(makeLocalChunkBeforeSync?: boolean) { - this.suspendExtraSync(); - await this.askUseNewAdapter(); - this.plugin.settings.isConfigured = true; - await this.suspendReflectingDatabase(); - await this.plugin.realizeSettingSyncMode(); - await this.resetLocalDatabase(); - await delay(1000); - await this.plugin.openDatabase(); - this.plugin.isReady = true; - if (makeLocalChunkBeforeSync) { - await this.plugin.createAllChunks(true); - } - await this.plugin.markRemoteResolved(); - await delay(500); - await this.plugin.replicateAllFromServer(true); - await delay(1000); - await this.plugin.replicateAllFromServer(true); - await this.resumeReflectingDatabase(); - await this.askHiddenFileConfiguration({ enableFetch: true }); - } - async fetchLocalWithRebuild() { - return await this.fetchLocal(true); - } - async rebuildRemote() { - this.suspendExtraSync(); - this.plugin.settings.isConfigured = true; - - await this.plugin.realizeSettingSyncMode(); - await this.plugin.markRemoteLocked(); - await this.plugin.tryResetRemoteDatabase(); - await this.plugin.markRemoteLocked(); - await delay(500); - await this.askHiddenFileConfiguration({ enableOverwrite: true }); - await delay(1000); - await this.plugin.replicateAllToServer(true); - await delay(1000); - await this.plugin.replicateAllToServer(true); - } - async rebuildEverything() { - this.suspendExtraSync(); - await this.askUseNewAdapter(); - this.plugin.settings.isConfigured = true; - await this.plugin.realizeSettingSyncMode(); - await this.resetLocalDatabase(); - await delay(1000); - await this.plugin.initializeDatabase(true); - await this.plugin.markRemoteLocked(); - await this.plugin.tryResetRemoteDatabase(); - await this.plugin.markRemoteLocked(); - await delay(500); - await this.askHiddenFileConfiguration({ enableOverwrite: true }); - await delay(1000); - await this.plugin.replicateAllToServer(true); - await delay(1000); - await this.plugin.replicateAllToServer(true); - - } -} diff --git a/src/features/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts similarity index 81% rename from src/features/CmdConfigSync.ts rename to src/features/ConfigSync/CmdConfigSync.ts index 13ae532..2c94712 100644 --- a/src/features/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -1,25 +1,27 @@ import { writable } from 'svelte/store'; -import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles, diff_match_patch } from "../deps.ts"; +import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles, diff_match_patch, Platform, addIcon } from "../../deps.ts"; -import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, AnyEntry, SavingEntry, diff_result } from "../lib/src/common/types.ts"; -import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../lib/src/common/types.ts"; -import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../common/types.ts"; -import { createBlob, createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, getDocDataAsArray, isDocContentSame, isLoadedEntry, isObjectDifferent } from "../lib/src/common/utils.ts"; -import { Logger } from "../lib/src/common/logger.ts"; -import { digestHash } from "../lib/src/string_and_binary/hash.ts"; -import { arrayBufferToBase64, decodeBinary, readString } from 'src/lib/src/string_and_binary/convert.ts'; -import { serialized, shareRunningResult } from "../lib/src/concurrency/lock.ts"; -import { LiveSyncCommands } from "./LiveSyncCommands.ts"; -import { stripAllPrefixes } from "../lib/src/string_and_binary/path.ts"; -import { EVEN, PeriodicProcessor, disposeMemoObject, isMarkedAsSameChanges, markChangesAreSame, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts"; -import { PluginDialogModal } from "../common/dialogs.ts"; -import { JsonResolveModal } from "../ui/JsonResolveModal.ts"; -import { QueueProcessor } from '../lib/src/concurrency/processor.ts'; -import { pluginScanningCount } from '../lib/src/mock_and_interop/stores.ts'; -import type ObsidianLiveSyncPlugin from '../main.ts'; +import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, AnyEntry, SavingEntry, diff_result } from "../../lib/src/common/types.ts"; +import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../../lib/src/common/types.ts"; +import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../../common/types.ts"; +import { createBlob, createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, getDocDataAsArray, isDocContentSame, isLoadedEntry, isObjectDifferent } from "../../lib/src/common/utils.ts"; +import { Logger } from "../../lib/src/common/logger.ts"; +import { digestHash } from "../../lib/src/string_and_binary/hash.ts"; +import { arrayBufferToBase64, decodeBinary, readString } from '../../lib/src/string_and_binary/convert.ts'; +import { serialized, shareRunningResult } from "../../lib/src/concurrency/lock.ts"; +import { LiveSyncCommands } from "../LiveSyncCommands.ts"; +import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts"; +import { EVEN, PeriodicProcessor, disposeMemoObject, isCustomisationSyncMetadata, isMarkedAsSameChanges, isPluginMetadata, markChangesAreSame, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../../common/utils.ts"; +import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts"; +import { QueueProcessor } from '../../lib/src/concurrency/processor.ts'; +import { pluginScanningCount } from '../../lib/src/mock_and_interop/stores.ts'; +import type ObsidianLiveSyncPlugin from '../../main.ts'; import { base64ToArrayBuffer, base64ToString } from 'octagonal-wheels/binary/base64'; -import { ConflictResolveModal } from '../ui/ConflictResolveModal.ts'; +import { ConflictResolveModal } from '../../modules/features/InteractiveConflictResolving/ConflictResolveModal.ts'; import { Semaphore } from 'octagonal-wheels/concurrency/semaphore'; +import type { IObsidianModule } from '../../modules/AbstractObsidianModule.ts'; +import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from '../../common/events.ts'; +import { PluginDialogModal } from "./PluginDialogModal.ts"; const d = "\u200b"; const d2 = "\n"; @@ -184,10 +186,10 @@ function deserialize(str: string[], def: T) { return o; } return JSON.parse(str.join("")) as T; - } catch (ex) { + } catch { try { return parseYaml(str.join("")); - } catch (ex) { + } catch { return def; } } @@ -322,13 +324,14 @@ export type PluginDataEx = { mtime: number, }; -export class ConfigSync extends LiveSyncCommands { +export class ConfigSync extends LiveSyncCommands implements IObsidianModule { constructor(plugin: ObsidianLiveSyncPlugin) { super(plugin); pluginScanningCount.onChanged((e) => { const total = e.value; pluginIsEnumerating.set(total != 0); }) + } get kvDB() { return this.plugin.kvDB; @@ -340,13 +343,16 @@ export class ConfigSync extends LiveSyncCommands { get useSyncPluginEtc() { return this.plugin.settings.usePluginEtc; } + $isThisModuleEnabled() { + return this.plugin.settings.usePluginSync; + } pluginDialog?: PluginDialogModal = undefined; periodicPluginSweepProcessor = new PeriodicProcessor(this.plugin, async () => await this.scanAllConfigFiles(false)); pluginList: IPluginDataExDisplay[] = []; showPluginSyncModal() { - if (!this.settings.usePluginSync) { + if (!this.$isThisModuleEnabled()) { return; } if (this.pluginDialog) { @@ -367,7 +373,14 @@ export class ConfigSync extends LiveSyncCommands { this.hidePluginSyncModal(); this.periodicPluginSweepProcessor?.disable(); } + addRibbonIcon = this.plugin.addRibbonIcon.bind(this.plugin); onload() { + addIcon( + "custom-sync", + ` + + ` + ); this.plugin.addCommand({ id: "livesync-plugin-dialog-ex", name: "Show customization sync dialog", @@ -375,6 +388,10 @@ export class ConfigSync extends LiveSyncCommands { this.showPluginSyncModal(); }, }); + this.addRibbonIcon("custom-sync", "Show Customization sync", () => { + this.showPluginSyncModal(); + }).addClass("livesync-ribbon-showcustom"); + eventHub.onEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, () => this.showPluginSyncModal()); } getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" { @@ -399,33 +416,41 @@ export class ConfigSync extends LiveSyncCommands { // Idea non-filter option? return this.getFileCategory(filePath) != ""; } - async onInitializeDatabase(showNotice: boolean) { - if (this.settings.usePluginSync) { - try { - Logger("Scanning customizations..."); - await this.scanAllConfigFiles(showNotice); - Logger("Scanning customizations : done"); - } catch (ex) { - Logger("Scanning customizations : failed"); - Logger(ex, LOG_LEVEL_VERBOSE); - } - - } - } - async beforeReplicate(showNotice: boolean) { - if (this.settings.autoSweepPlugins && this.settings.usePluginSync) { + async $everyOnDatabaseInitialized(showNotice: boolean) { + if (!this.$isThisModuleEnabled()) return true; + try { + Logger("Scanning customizations..."); await this.scanAllConfigFiles(showNotice); + Logger("Scanning customizations : done"); + } catch (ex) { + Logger("Scanning customizations : failed"); + Logger(ex, LOG_LEVEL_VERBOSE); } + return true; } - async onResume() { - if (this.plugin.suspended) { - return; + async $everyBeforeReplicate(showNotice: boolean) { + if (!this.$isThisModuleEnabled()) return true; + if (this.settings.autoSweepPlugins) { + await this.scanAllConfigFiles(showNotice); + return true; } - if (this.settings.autoSweepPlugins && this.settings.usePluginSync) { + return true; + } + async $everyOnResumeProcess(): Promise { + if (!this.$isThisModuleEnabled()) return true; + if (this.$isMainSuspended()) { + return true; + } + if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(false); } this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0); - + return true; + } + $everyAfterResumeProcess(): Promise { + const q = activeDocument.querySelector(`.livesync-ribbon-showcustom`); + q?.toggleClass("sls-hidden", !this.$isThisModuleEnabled()); + return Promise.resolve(true); } async reloadPluginList(showMessage: boolean) { this.pluginList = []; @@ -699,7 +724,7 @@ export class ConfigSync extends LiveSyncCommands { const v2Path = (prefixPath + relativeFilename) as FilePathWithPrefix; // console.warn(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`); Logger(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE); - const newId = await this.plugin.path2id(v2Path); + const newId = await this.plugin.$$path2id(v2Path); // const buf = const data = createBlob([DUMMY_HEAD, DUMMY_END, ...getDocDataAsArray(f.data)]); @@ -729,7 +754,7 @@ export class ConfigSync extends LiveSyncCommands { } async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise { - if (!this.settings.usePluginSync) { + if (!this.$isThisModuleEnabled()) { this.pluginScanProcessor.clearQueue(); this.pluginList = []; pluginList.set(this.pluginList) @@ -843,9 +868,9 @@ export class ConfigSync extends LiveSyncCommands { const filename = data.files[0].filename; Logger(`Applying ${filename} of ${data.displayName || data.name}..`); const path = `${baseDir}/${filename}` as FilePath; - await this.vaultAccess.ensureDirectory(path); + await this.plugin.storageAccess.ensureDir(path); // If the content has applied, modified time will be updated to the current time. - await this.vaultAccess.adapterWrite(path, content); + await this.plugin.storageAccess.writeHiddenFileAuto(path, content); await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName); } else { @@ -856,13 +881,15 @@ export class ConfigSync extends LiveSyncCommands { const path = `${baseDir}/${f.filename}` as FilePath; Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`); // const contentEach = createBlob(f.data); - this.vaultAccess.ensureDirectory(path); + await this.plugin.storageAccess.ensureDir(path); if (f.datatype == "newnote") { let oldData; try { - oldData = await this.vaultAccess.adapterReadBinary(path); + oldData = await this.plugin.storageAccess.readHiddenFileBinary(path); } catch (ex) { + Logger(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); + Logger(ex, LOG_LEVEL_VERBOSE); oldData = new ArrayBuffer(0); } const content = base64ToArrayBuffer(f.data); @@ -870,12 +897,14 @@ export class ConfigSync extends LiveSyncCommands { Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); continue; } - await this.vaultAccess.adapterWrite(path, content, stat); + await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat); } else { let oldData; try { - oldData = await this.vaultAccess.adapterRead(path); + oldData = await this.plugin.storageAccess.readHiddenFileText(path); } catch (ex) { + Logger(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); + Logger(ex, LOG_LEVEL_VERBOSE); oldData = ""; } const content = getDocData(f.data); @@ -883,7 +912,7 @@ export class ConfigSync extends LiveSyncCommands { Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); continue; } - await this.vaultAccess.adapterWrite(path, content, stat); + await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat); } Logger(`Applied ${f.filename} of ${data.displayName || data.name}..`); await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName); @@ -916,12 +945,12 @@ export class ConfigSync extends LiveSyncCommands { try { // console.dir(f); const path = `${baseDir}/${f.filename}`; - await this.vaultAccess.ensureDirectory(path); + await this.plugin.storageAccess.ensureDir(path); if (!content) { const dt = decodeBinary(f.data); - await this.vaultAccess.adapterWrite(path, dt); + await this.plugin.storageAccess.writeHiddenFileAuto(path, dt); } else { - await this.vaultAccess.adapterWrite(path, content); + await this.plugin.storageAccess.writeHiddenFileAuto(path, content); } Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`); @@ -951,7 +980,7 @@ export class ConfigSync extends LiveSyncCommands { Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); } } else if (data.category == "CONFIG") { - this.plugin.askReload(); + this.plugin.$$askReload(); } return true; } catch (ex) { @@ -983,79 +1012,76 @@ export class ConfigSync extends LiveSyncCommands { return true; } catch (ex) { Logger(`Failed to delete: ${data.documentPath}`, LOG_LEVEL_NOTICE); + Logger(ex, LOG_LEVEL_VERBOSE); return false; } } - async parseReplicationResultItem(docs: PouchDB.Core.ExistingDocument) { - if (docs._id.startsWith(ICXHeader)) { - if (this.plugin.settings.usePluginSync) { - await this.updatePluginList(false, (docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath((docs as AnyEntry))); - } - if (this.plugin.settings.usePluginSync && this.plugin.settings.notifyPluginOrSettingUpdated) { - if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) { - const fragment = createFragment((doc) => { - doc.createEl("span", undefined, (a) => { - a.appendText(`Some configuration has been arrived, Press `); - a.appendChild(a.createEl("a", undefined, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - this.showPluginSyncModal(); - }); - })); + async $anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument) { + if (!docs._id.startsWith(ICXHeader)) return undefined; + if (this.$isThisModuleEnabled()) { + await this.updatePluginList(false, (docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath((docs as AnyEntry))); + } + if (this.$isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) { + if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) { + const fragment = createFragment((doc) => { + doc.createEl("span", undefined, (a) => { + a.appendText(`Some configuration has been arrived, Press `); + a.appendChild(a.createEl("a", undefined, (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + this.showPluginSyncModal(); + }); + })); - a.appendText(` to open the config sync dialog , or press elsewhere to dismiss this message.`); - }); + a.appendText(` to open the config sync dialog , or press elsewhere to dismiss this message.`); }); + }); - const updatedPluginKey = "popupUpdated-plugins"; - scheduleTask(updatedPluginKey, 1000, async () => { - const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0)); + const updatedPluginKey = "popupUpdated-plugins"; + scheduleTask(updatedPluginKey, 1000, async () => { + const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0)); + //@ts-ignore + const isShown = popup?.noticeEl?.isShown(); + if (!isShown) { + memoObject(updatedPluginKey, new Notice(fragment, 0)); + } + scheduleTask(updatedPluginKey + "-close", 20000, () => { + const popup = retrieveMemoObject(updatedPluginKey); + if (!popup) + return; //@ts-ignore - const isShown = popup?.noticeEl?.isShown(); - if (!isShown) { - memoObject(updatedPluginKey, new Notice(fragment, 0)); + if (popup?.noticeEl?.isShown()) { + popup.hide(); } - scheduleTask(updatedPluginKey + "-close", 20000, () => { - const popup = retrieveMemoObject(updatedPluginKey); - if (!popup) - return; - //@ts-ignore - if (popup?.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(updatedPluginKey); - }); + disposeMemoObject(updatedPluginKey); }); - } + }); } - return true; } - return false; + return true; } - async realizeSettingSyncMode(): Promise { + async $everyRealizeSettingSyncMode(): Promise { this.periodicPluginSweepProcessor?.disable(); - if (this.plugin.suspended) - return; - if (!this.settings.usePluginSync) { - return; - } + if (!this.$isMainReady) return true; + if (!this.$isMainSuspended()) return true; + if (!this.$isThisModuleEnabled()) return true; if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(false); } this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0); - return; + return true; } recentProcessedInternalFiles = [] as string[]; async makeEntryFromFile(path: FilePath): Promise { - const stat = await this.vaultAccess.adapterStat(path); + const stat = await this.plugin.storageAccess.statHidden(path); let version: string | undefined; let displayName: string | undefined; if (!stat) { return false; } - const contentBin = await this.vaultAccess.adapterReadBinary(path); + const contentBin = await this.plugin.storageAccess.readHiddenFileBinary(path); let content: string[]; try { content = await arrayBufferToBase64(contentBin); @@ -1071,6 +1097,7 @@ export class ConfigSync extends LiveSyncCommands { } } catch (ex) { Logger(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL_INFO); + Logger(ex, LOG_LEVEL_VERBOSE); } } } catch (ex) { @@ -1096,12 +1123,12 @@ export class ConfigSync extends LiveSyncCommands { const prefixedFileName = vf; const id = await this.path2id(prefixedFileName); - const stat = await this.vaultAccess.adapterStat(path); + const stat = await this.plugin.storageAccess.statHidden(path); if (!stat) { return false; } const mtime = stat.mtime; - const content = await this.vaultAccess.adapterReadBinary(path); + const content = await this.plugin.storageAccess.readHiddenFileBinary(path); const contentBlob = createBlob([DUMMY_HEAD, DUMMY_END, ...await arrayBufferToBase64(content)]); // const contentBlob = createBlob(content); try { @@ -1258,7 +1285,7 @@ export class ConfigSync extends LiveSyncCommands { const d = await deserialize(getDocDataAsArray(oldC.data), {}) as PluginDataEx; if (d.files.length == dt.files.length) { const diffs = (d.files.map(previous => ({ prev: previous, curr: dt.files.find(e => e.filename == previous.filename) })).map(async e => { - try { return await isDocContentSame(e.curr?.data ?? [], e.prev.data) } catch (_) { return false } + try { return await isDocContentSame(e.curr?.data ?? [], e.prev.data) } catch { return false } })) const isSame = (await Promise.all(diffs)).every(e => e == true); if (isSame) { @@ -1291,10 +1318,16 @@ export class ConfigSync extends LiveSyncCommands { }) } + async $anyProcessOptionalFileEvent(path: FilePath): Promise { + return await this.watchVaultRawEventsAsync(path); + } + async watchVaultRawEventsAsync(path: FilePath) { - if (!this.settings.usePluginSync) return false; + // if (!this.$isMainReady) return true; + // if (!this.$isMainSuspended()) return true; + if (!this.$isThisModuleEnabled()) return true; if (!this.isTargetPath(path)) return false; - const stat = await this.vaultAccess.adapterStat(path); + const stat = await this.plugin.storageAccess.statHidden(path); // Make sure that target is a file. if (stat && stat.type != "file") return false; @@ -1305,12 +1338,14 @@ export class ConfigSync extends LiveSyncCommands { ).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) { Logger(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE); - return; + // This file could be handled by the other module. + return false; } const storageMTime = ~~((stat && stat.mtime || 0) / 1000); const key = `${path}-${storageMTime}`; if (this.recentProcessedInternalFiles.contains(key)) { // If recently processed, it may caused by self. + // return true to prevent pass the event to the next. return true; } this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100); @@ -1319,6 +1354,9 @@ export class ConfigSync extends LiveSyncCommands { scheduleTask(keySchedule, 100, async () => { await this.storeCustomizationFiles(path); }) + // Okay, it may handled after 100ms. + // This was my own job. + return true; } @@ -1383,8 +1421,7 @@ export class ConfigSync extends LiveSyncCommands { }) } await Promise.all(taskExtra.map(e => e())); - - this.updatePluginList(false).then(/* fire and forget */); + fireAndForget(() => this.updatePluginList(false)); } else { const files = filesAll.filter(e => this.isTargetPath(e)).map(e => ({ key: this.filenameToUnifiedKey(e), file: e })); const virtualPathsOfLocalFiles = [...new Set(files.map(e => e.key))]; @@ -1402,7 +1439,7 @@ export class ConfigSync extends LiveSyncCommands { for (const vp of deleteCandidate) { await this.deleteConfigOnDatabase(vp); } - this.updatePluginList(false).then(/* fire and forget */); + fireAndForget(() => this.updatePluginList(false)) } }); } @@ -1450,7 +1487,99 @@ export class ConfigSync extends LiveSyncCommands { return filenames as FilePath[]; } + async $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean, enableOverwrite?: boolean }): Promise { + await this._askHiddenFileConfiguration(opt); + return true; + } + async _askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { + const message = `Would you like to enable \`Customization sync\`? +This feature allows you to sync your customisations -- such as configurations, themes, snippets, and plugins -- across your devices in a fully controlled manner, unlike the fully automatic behaviour of hidden file synchronisation. +You may use this feature alongside hidden file synchronisation. When both features are enabled, items configured as \`Automatic\` in this feature will be managed by hidden file synchronisation. + +Do not worry, you will be prompted to enable or keep disabled hidden file synchronisation after this dialogue. + +Of course, you can enable or disable this feature at any time. +` + const CHOICE_CUSTOMIZE = "Yes, Enable it"; + const CHOICE_DISABLE = "No, Disable it"; + const CHOICE_DISMISS = "Later"; + const choices = []; + + choices.push(CHOICE_CUSTOMIZE); + choices.push(CHOICE_DISABLE); + choices.push(CHOICE_DISMISS); + + const ret = await this.plugin.confirm.confirmWithMessage("Customisation sync", message, choices, CHOICE_DISMISS, 40); + if (ret == CHOICE_CUSTOMIZE) { + await this.configureHiddenFileSync("CUSTOMIZE"); + } else if (ret == CHOICE_DISABLE) { + await this.configureHiddenFileSync("DISABLE_CUSTOM"); + } + + } + + $anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise { + if (isPluginMetadata(path)) { + return Promise.resolve("newer") + } + if (isCustomisationSyncMetadata(path)) { + return Promise.resolve("newer"); + } + return Promise.resolve(false); + } + + $allSuspendExtraSync(): Promise { + if (this.plugin.settings.usePluginSync || this.plugin.settings.autoSweepPlugins) { + Logger("Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) + this.plugin.settings.usePluginSync = false; + this.plugin.settings.autoSweepPlugins = false; + } + return Promise.resolve(true); + } + + async $anyConfigureOptionalSyncFeature(mode: "CUSTOMIZE" | "DISABLE" | "DISABLE_CUSTOM") { + await this.configureHiddenFileSync(mode); + } + async configureHiddenFileSync(mode: "CUSTOMIZE" | "DISABLE" | "DISABLE_CUSTOM") { + if (mode == "DISABLE") { + this.plugin.settings.usePluginSync = false; + await this.plugin.saveSettings(); + return; + } + + if (mode == "CUSTOMIZE") { + if (!this.plugin.deviceAndVaultName) { + let name = await this.plugin.confirm.askString("Device name", "Please set this device name", `desktop`); + if (!name) { + if (Platform.isAndroidApp) { + name = "android-app" + } else if (Platform.isIosApp) { + name = "ios" + } else if (Platform.isMacOS) { + name = "macos" + } else if (Platform.isMobileApp) { + name = "mobile-app" + } else if (Platform.isMobile) { + name = "mobile" + } else if (Platform.isSafari) { + name = "safari" + } else if (Platform.isDesktop) { + name = "desktop" + } else if (Platform.isDesktopApp) { + name = "desktop-app" + } else { + name = "unknown" + } + name = name + Math.random().toString(36).slice(-4); + } + this.plugin.deviceAndVaultName = name; + } + this.plugin.settings.usePluginSync = true; + await this.plugin.saveSettings(); + await this.scanAllConfigFiles(true); + } + } async getFiles( path: string, diff --git a/src/ui/components/PluginCombo.svelte b/src/features/ConfigSync/PluginCombo.svelte similarity index 96% rename from src/ui/components/PluginCombo.svelte rename to src/features/ConfigSync/PluginCombo.svelte index 3624061..bd07303 100644 --- a/src/ui/components/PluginCombo.svelte +++ b/src/features/ConfigSync/PluginCombo.svelte @@ -1,10 +1,10 @@ + +

TESTING BENCH: Self-hosted LiveSync

+ +

Module Checks

+ + + + + +{#each resultLines as [result, line, message]} +
+ [{result ? "PASS" : "FAILED"}] {line} +
{message}
+
+{/each} + +

Synchronisation Result Status

+
{syncStatus.join("\n")}
+ +

Performance test

+ + + +
+ + diff --git a/src/tests/TestPaneView.ts b/src/modules/extras/devUtil/TestPaneView.ts similarity index 75% rename from src/tests/TestPaneView.ts rename to src/modules/extras/devUtil/TestPaneView.ts index e69997c..a0b9b17 100644 --- a/src/tests/TestPaneView.ts +++ b/src/modules/extras/devUtil/TestPaneView.ts @@ -3,13 +3,15 @@ import { WorkspaceLeaf } from "obsidian"; import TestPaneComponent from "./TestPane.svelte" -import type ObsidianLiveSyncPlugin from "../main" +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" navigation = true; @@ -18,9 +20,10 @@ export class TestPaneView extends ItemView { return "view-log"; } - constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) { + constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin, moduleDev: ModuleDev) { super(leaf); this.plugin = plugin; + this.moduleDev = moduleDev; } @@ -37,13 +40,16 @@ export class TestPaneView extends ItemView { this.component = new TestPaneComponent({ target: this.contentEl, props: { - plugin: this.plugin + plugin: this.plugin, + moduleDev: this.moduleDev }, }); + await Promise.resolve(); } // eslint-disable-next-line require-await async onClose() { this.component?.$destroy(); + await Promise.resolve(); } } diff --git a/src/tests/testUtils.ts b/src/modules/extras/devUtil/testUtils.ts similarity index 80% rename from src/tests/testUtils.ts rename to src/modules/extras/devUtil/testUtils.ts index 84b9e8d..869d1fa 100644 --- a/src/tests/testUtils.ts +++ b/src/modules/extras/devUtil/testUtils.ts @@ -1,6 +1,6 @@ -import { fireAndForget } from "src/lib/src/common/utils"; -import { serialized } from "src/lib/src/concurrency/lock"; -import type ObsidianLiveSyncPlugin from "src/main"; +import { fireAndForget } from "../../../lib/src/common/utils.ts"; +import { serialized } from "../../../lib/src/concurrency/lock.ts"; +import type ObsidianLiveSyncPlugin from "../../../main.ts"; let plugin: ObsidianLiveSyncPlugin; export function enableTestFunction(plugin_: ObsidianLiveSyncPlugin) { @@ -37,8 +37,9 @@ export function addDebugFileLog(message: any, stackLog = false) { // const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || ""); // const out try { - await plugin.vaultAccess.adapterAppend(plugin.app.vault.configDir + "/ls-debug/" + outFile, JSON.stringify(out) + "\n") - } catch (ex) { + await plugin.storageAccess.appendHiddenFile(plugin.app.vault.configDir + "/ls-debug/" + outFile, JSON.stringify(out) + "\n") + } catch { + //NO OP } })); diff --git a/src/tests/tests.ts b/src/modules/extras/devUtil/tests.ts similarity index 83% rename from src/tests/tests.ts rename to src/modules/extras/devUtil/tests.ts index 1fae9fa..f4c5119 100644 --- a/src/tests/tests.ts +++ b/src/modules/extras/devUtil/tests.ts @@ -1,5 +1,5 @@ -import { Trench } from "../lib/src/memory/memutil.ts"; -import type ObsidianLiveSyncPlugin from "../main.ts"; +import { Trench } from "../../../lib/src/memory/memutil.ts"; +import type ObsidianLiveSyncPlugin from "../../../main.ts"; type MeasureResult = [times: number, spent: number]; type NamedMeasureResult = [name: string, result: MeasureResult]; const measures = new Map(); @@ -30,9 +30,10 @@ async function measure(name: string, proc: () => (void | Promise), times: return [name, measures.get(name) as MeasureResult]; } -// eslint-disable-next-line require-await +// 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"); + } export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { clearResult("trench"); @@ -43,7 +44,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { await p(); })); { - const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/10kb.png"); + 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); @@ -51,7 +52,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { })); } { - const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/100kb.jpeg"); + 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); @@ -59,7 +60,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { })); } { - const testBinary = await plugin.vaultAccess.adapterReadBinary("testdata/1mb.png"); + 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); diff --git a/src/ui/DocumentHistoryModal.ts b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts similarity index 82% rename from src/ui/DocumentHistoryModal.ts rename to src/modules/features/DocumentHistory/DocumentHistoryModal.ts index 7a830aa..9d71e60 100644 --- a/src/ui/DocumentHistoryModal.ts +++ b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts @@ -1,12 +1,13 @@ -import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../deps.ts"; -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 { Logger } from "../lib/src/common/logger.ts"; -import { isErrorOfMissingDoc } from "../lib/src/pouchdb/utils_couchdb.ts"; -import { getDocData, readContent } from "../lib/src/common/utils.ts"; -import { isPlainText, stripPrefix } from "../lib/src/string_and_binary/path.ts"; +import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../../../deps.ts"; +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 { 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"; +import { isPlainText, stripPrefix } from "../../../lib/src/string_and_binary/path.ts"; +import { scheduleOnceIfDuplicated } from "octagonal-wheels/concurrency/lock"; function isImage(path: string) { const ext = path.split(".").splice(-1)[0].toLowerCase(); @@ -31,6 +32,7 @@ function readDocument(w: LoadedEntry) { try { return readString(new Uint8Array(decodeBinary(w.data))); } catch (ex) { + Logger(ex, LOG_LEVEL_VERBOSE); // NO OP. } return getDocData(w.data); @@ -59,7 +61,7 @@ export class DocumentHistoryModal extends Modal { this.id = id; this.initialRev = revision; if (!file && id) { - this.file = this.plugin.id2path(id); + this.file = this.plugin.$$id2path(id); } if (localStorage.getItem("ols-history-highlightdiff") == "1") { this.showDiff = true; @@ -68,7 +70,7 @@ export class DocumentHistoryModal extends Modal { async loadFile(initialRev?: string) { if (!this.id) { - this.id = await this.plugin.path2id(this.file); + this.id = await this.plugin.$$path2id(this.file); } const db = this.plugin.localDatabase; try { @@ -207,10 +209,10 @@ export class DocumentHistoryModal extends Modal { divView.createEl("input", { type: "range" }, (e) => { this.range = e; e.addEventListener("change", (e) => { - this.loadRevs(); + void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); }); e.addEventListener("input", (e) => { - this.loadRevs(); + void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); }); }); contentEl @@ -224,7 +226,7 @@ export class DocumentHistoryModal extends Modal { checkbox.addEventListener("input", (evt: any) => { this.showDiff = checkbox.checked; localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : ""); - this.loadRevs(); + void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs()); }); }) ); @@ -234,7 +236,7 @@ export class DocumentHistoryModal extends Modal { .addClass("op-info"); this.info = contentEl.createDiv(""); this.info.addClass("op-info"); - this.loadFile(this.initialRev); + fireAndForget(async () => await this.loadFile(this.initialRev)); const div = contentEl.createDiv({ text: "Loading old revisions..." }); this.contentView = div; div.addClass("op-scrollable"); @@ -242,9 +244,11 @@ export class DocumentHistoryModal extends Modal { const buttons = contentEl.createDiv(""); buttons.createEl("button", { text: "Copy to clipboard" }, (e) => { e.addClass("mod-cta"); - e.addEventListener("click", async () => { - await navigator.clipboard.writeText(this.currentText); - Logger(`Old content copied to clipboard`, LOG_LEVEL_NOTICE); + e.addEventListener("click", () => { + fireAndForget(async () => { + await navigator.clipboard.writeText(this.currentText); + Logger(`Old content copied to clipboard`, LOG_LEVEL_NOTICE); + }); }); }); const focusFile = async (path: string) => { @@ -258,21 +262,23 @@ export class DocumentHistoryModal extends Modal { } buttons.createEl("button", { text: "Back to this revision" }, (e) => { e.addClass("mod-cta"); - e.addEventListener("click", async () => { - // const pathToWrite = this.plugin.id2path(this.id, true); - const pathToWrite = stripPrefix(this.file); - if (!isValidPath(pathToWrite)) { - Logger("Path is not valid to write content.", LOG_LEVEL_INFO); - return; - } - if (!this.currentDoc) { - Logger("No active file loaded.", LOG_LEVEL_INFO); - return; - } - const d = readContent(this.currentDoc); - await this.plugin.vaultAccess.adapterWrite(pathToWrite, d); - await focusFile(pathToWrite); - this.close(); + e.addEventListener("click", () => { + fireAndForget(async () => { + // const pathToWrite = this.plugin.id2path(this.id, true); + const pathToWrite = stripPrefix(this.file); + if (!isValidPath(pathToWrite)) { + Logger("Path is not valid to write content.", LOG_LEVEL_INFO); + return; + } + if (!this.currentDoc) { + Logger("No active file loaded.", LOG_LEVEL_INFO); + return; + } + const d = readContent(this.currentDoc); + await this.plugin.storageAccess.writeHiddenFileAuto(pathToWrite, d); + await focusFile(pathToWrite); + this.close(); + }); }); }); } diff --git a/src/ui/GlobalHistory.svelte b/src/modules/features/GlobalHistory/GlobalHistory.svelte similarity index 91% rename from src/ui/GlobalHistory.svelte rename to src/modules/features/GlobalHistory/GlobalHistory.svelte index 0b6659a..30806d9 100644 --- a/src/ui/GlobalHistory.svelte +++ b/src/modules/features/GlobalHistory/GlobalHistory.svelte @@ -1,12 +1,13 @@ - -

TESTBENCH: Self-hosted LiveSync

- -

Function check

-
{functionCheckResult}
- -

Performance test

- - - -
- - diff --git a/terser.config.mjs b/terser.config.mjs index 7554bfe..79ce9ae 100644 --- a/terser.config.mjs +++ b/terser.config.mjs @@ -12,8 +12,8 @@ const terserOption = { } : {}, format: { - indent_level: 2, - beautify: true, + // indent_level: 2, + // beautify: true, comments: "some", ecma: 2018, preamble: banner, diff --git a/tsconfig.json b/tsconfig.json index 19442a2..0433089 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,30 +8,16 @@ "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", - "types": [ - "svelte", - "node" - ], + "types": ["svelte", "node"], // "importsNotUsedAsValues": "error", "importHelpers": false, "alwaysStrict": true, "allowImportingTsExtensions": true, "noEmit": true, - "lib": [ - "es2018", - "DOM", - "ES5", - "ES6", - "ES7", - "es2019.array", - "ES2020.BigInt", - "ESNext.Intl" - ] + "lib": ["es2018", "DOM", "ES5", "ES6", "ES7", "es2019.array", "ES2021.WeakRef", "ES2020.BigInt", "ESNext.Intl"], + "strictBindCallApply": true, + "strictFunctionTypes": true }, - "include": [ - "**/*.ts" - ], - "exclude": [ - "pouchdb-browser-webpack" - ] -} \ No newline at end of file + "include": ["**/*.ts"], + "exclude": ["pouchdb-browser-webpack", "utils", "src/modules/coreObsidian/devUtil/tests.ts", "src/lib/src/API/**"] +} From 095a3d20fb8fa5db443a1798d335f5ce1dd24b4c Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 17 Oct 2024 09:57:42 +0100 Subject: [PATCH 2/8] 0.24.0.dev-rc2 --- .eslintrc | 2 +- manifest-beta.json | 10 ++ package-lock.json | 4 +- package.json | 4 +- src/lib | 2 +- src/modules/AbstractModule.ts | 2 +- .../coreObsidian/ModuleFileAccessObsidian.ts | 4 - .../storageLib/StorageEventManager.ts | 84 +----------- .../essentialObsidian/ModuleObsidianAPI.ts | 13 +- src/modules/features/ModuleLog.ts | 9 +- tsconfig.json | 2 +- updates.md | 125 +++++------------- updates_old.md | 75 +++++++++++ 13 files changed, 140 insertions(+), 196 deletions(-) create mode 100644 manifest-beta.json diff --git a/.eslintrc b/.eslintrc index f9ca43f..64c653b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,7 +7,7 @@ "sourceType": "module", "project": ["tsconfig.json"] }, - "ignorePatterns": ["src/lib/src/API/*.ts"], + "ignorePatterns": [], "rules": { "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ diff --git a/manifest-beta.json b/manifest-beta.json new file mode 100644 index 0000000..8c4cc40 --- /dev/null +++ b/manifest-beta.json @@ -0,0 +1,10 @@ +{ + "id": "obsidian-livesync", + "name": "Self-hosted LiveSync", + "version": "0.24.0.dev-rc2", + "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", + "authorUrl": "https://github.com/vrtmrz", + "isDesktopOnly": false +} diff --git a/package-lock.json b/package-lock.json index aa5ee51..6369a8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.24.0.rc1", + "version": "0.24.0.dev-rc2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.24.0.rc1", + "version": "0.24.0.dev-rc2", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", diff --git a/package.json b/package.json index 4e3b261..9c8ef46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.24.0.rc1", + "version": "0.24.0.dev-rc2", "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", @@ -73,4 +73,4 @@ "xxhash-wasm": "0.4.2", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" } -} \ No newline at end of file +} diff --git a/src/lib b/src/lib index ed2b0c0..92d7b03 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit ed2b0c00fcf98a0fad185e204fbc044e302f9ea1 +Subproject commit 92d7b03916cce550fba4fc2334d7eb4d57960e00 diff --git a/src/modules/AbstractModule.ts b/src/modules/AbstractModule.ts index 2a0a141..c757de1 100644 --- a/src/modules/AbstractModule.ts +++ b/src/modules/AbstractModule.ts @@ -131,7 +131,7 @@ export function injectModules(target: T, modules: ICoreMo export abstract class AbstractModule { _log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => { if (typeof msg === "string" && level !== LOG_LEVEL_NOTICE) { - msg = `[${this.constructor.name}] ${msg}`; + msg = `[${this.constructor.name}]\u{200A} ${msg}`; } // console.log(msg); Logger(msg, level, key); diff --git a/src/modules/coreObsidian/ModuleFileAccessObsidian.ts b/src/modules/coreObsidian/ModuleFileAccessObsidian.ts index 7f42506..1c885de 100644 --- a/src/modules/coreObsidian/ModuleFileAccessObsidian.ts +++ b/src/modules/coreObsidian/ModuleFileAccessObsidian.ts @@ -18,10 +18,6 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements } $everyOnFirstInitialize(): Promise { this.vaultManager.beginWatch(); - this.plugin.totalQueued = this.vaultManager.totalQueued; - this.plugin.batched = this.vaultManager.batched; - this.plugin.processing = this.vaultManager.processing; - return Promise.resolve(true); } $allOnUnload(): Promise { diff --git a/src/modules/coreObsidian/storageLib/StorageEventManager.ts b/src/modules/coreObsidian/storageLib/StorageEventManager.ts index e6b0380..3a4b926 100644 --- a/src/modules/coreObsidian/storageLib/StorageEventManager.ts +++ b/src/modules/coreObsidian/storageLib/StorageEventManager.ts @@ -6,7 +6,6 @@ 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 { reactiveSource, type ReactiveSource } from "../../../lib/src/dataobject/reactive.ts"; import { Semaphore } from "../../../lib/src/concurrency/semaphore.ts"; import type { LiveSyncCore } from "../../../main.ts"; import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "./utilObsidian.ts"; @@ -29,17 +28,12 @@ export abstract class StorageEventManager { abstract appendQueue(items: FileEvent[], ctx?: any): Promise; abstract cancelQueue(key: string): void; abstract isWaiting(filename: FilePath): boolean; - abstract totalQueued: ReactiveSource; - abstract batched: ReactiveSource; - abstract processing: ReactiveSource; } export class StorageEventManagerObsidian extends StorageEventManager { - totalQueued = reactiveSource(0); - batched = reactiveSource(0); - processing = reactiveSource(0); + plugin: ObsidianLiveSyncPlugin; core: LiveSyncCore; @@ -330,9 +324,10 @@ export class StorageEventManagerObsidian extends StorageEventManager { } updateStatus() { const allItems = this.bufferedQueuedItems.filter(e => !e.cancelled) - this.batched.value = allItems.filter(e => e.batched && !e.skipBatchWait).length; - this.processing.value = this.processingCount; - this.totalQueued.value = allItems.length - this.batched.value; + 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; } async handleFileEvent(queue: FileEventItem): Promise { @@ -340,7 +335,6 @@ export class StorageEventManagerObsidian extends StorageEventManager { const lockKey = `handleFile:${file.path}`; return await serialized(lockKey, async () => { // TODO CHECK - // console.warn(lockKey); const key = `file-last-proc-${queue.type}-${file.path}`; const last = Number(await this.core.kvDB.get(key) || 0); if (queue.type == "INTERNAL" || file.isInternal) { @@ -362,77 +356,11 @@ export class StorageEventManagerObsidian extends StorageEventManager { this.cancelRelativeEvent(queue); return; } - // if (queue.type == "CREATE" || queue.type == "CHANGED") { - // // eventHub.emitEvent("event-file-changed", { file: targetFile, automated: true }); - - // if (!await this.core.updateIntoDB(targetFile, undefined)) { - // Logger(`STORAGE -> DB: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL_INFO); - // // cancel running queues and remove one of atomic operation - // this.cancelRelativeEvent(queue); - // return; - // } - // } - // if (queue.type == "RENAME") { - // // Obsolete , can be called? - // await this.renameVaultItem(targetFile, queue.args.oldPath); - // } - // } - // await this.core.deleteFromDBbyPath(file.path); - // mtime = file.stat.mtime - 1; - // const keyD1 = `file-last-proc-CREATE-${file.path}`; - // const keyD2 = `file-last-proc-CHANGED-${file.path}`; - // await this.core.kvDB.set(keyD1, mtime); - // await this.core.kvDB.set(keyD2, mtime); - // } else { - // const targetFile = this.core.storageAccess.getFileStub(file.path); - // if (!(targetFile)) { - // Logger(`Target file was not found: ${file.path}`, LOG_LEVEL_INFO); - // return; - // } - // if (file.stat.mtime == last) { - // Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE); - // return; - // } - - // // const cache = queue.args.cache; - // if (queue.type == "CREATE" || queue.type == "CHANGED") { - // eventHub.emitEvent("event-file-changed", { file: targetFile, automated: true }); - // // fireAndForget(() => this.addOnObsidianUI.checkAndApplySettingFromMarkdown(queue.args.file.path, true)); - // const keyD1 = `file-last-proc-DELETED-${file.path}`; - // await this.core.kvDB.set(keyD1, mtime); - // if (!await this.core.updateIntoDB(targetFile, undefined)) { - // Logger(`STORAGE -> DB: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL_INFO); - // // cancel running queues and remove one of atomic operation - // this.cancelRelativeEvent(queue); - // return; - // } - // } - // if (queue.type == "RENAME") { - // // Obsolete , can be called? - // await this.renameVaultItem(targetFile, queue.args.oldPath); - // } - // } - // await this.core.kvDB.set(key, mtime); } } }); } - // async renameVaultItem(file: UXFileInfoStub, oldFile: any, cache?: CacheData): Promise { - // Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL_VERBOSE); - // if (!file.isFolder) { - // try { - // // Logger(`RENAMING.. ${file.path} into db`); - // if (await this.core.updateIntoDB(file, cache)) { - // // Logger(`deleted ${oldFile} from db`); - // await this.core.deleteFromDBbyPath(oldFile); - // } else { - // Logger(`Could not save new file: ${file.path} `, LOG_LEVEL_NOTICE); - // } - // } catch (ex) { - // Logger(ex); - // } - // } - // } + cancelRelativeEvent(item: FileEventItem): void { this.cancelQueue(item.key); } diff --git a/src/modules/essentialObsidian/ModuleObsidianAPI.ts b/src/modules/essentialObsidian/ModuleObsidianAPI.ts index e3388aa..2d5910d 100644 --- a/src/modules/essentialObsidian/ModuleObsidianAPI.ts +++ b/src/modules/essentialObsidian/ModuleObsidianAPI.ts @@ -115,19 +115,8 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi } } - // -old implementation - try { - // const orgHeaders = opts?.headers; - // const newHeaders = new Headers(); - // newHeaders.append("authorization", authHeader); - // if (orgHeaders && typeof orgHeaders.forEach !== "string") { - // const items = Object.entries(orgHeaders); - // items.forEach(([key, value]) => newHeaders.append(key, value)); - // } - // newHeaders.append("ngrok-skip-browser-warning", "123"); - // opts!.headers = newHeaders; - DEV: { + if (this.settings.enableDebugTools) { // Issue #407 (opts!.headers as Headers).append("ngrok-skip-browser-warning", "123"); } diff --git a/src/modules/features/ModuleLog.ts b/src/modules/features/ModuleLog.ts index c25beeb..17a601f 100644 --- a/src/modules/features/ModuleLog.ts +++ b/src/modules/features/ModuleLog.ts @@ -212,13 +212,14 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule const { message, status } = this.statusBarLabels.value; // const recent = logMessages.value; const newMsg = message; - const newLog = this.settings?.showOnlyIconsOnEditor ? "" : status; + let newLog = this.settings?.showOnlyIconsOnEditor ? "" : status; + const moduleTagEnd = newLog.indexOf(`]\u{200A}`); + if (moduleTagEnd != -1) { + newLog = newLog.substring(moduleTagEnd + 2); + } this.statusBar?.setText(newMsg.split("\n")[0]); if (this.settings?.showStatusOnEditor && this.statusDiv) { - // const root = activeDocument.documentElement; - // root.style.setProperty("--sls-log-text", "'" + (newMsg + "\\A " + newLog) + "'"); - // this.statusDiv.innerText = newMsg + "\\A " + newLog; if (this.settings.showLongerLogInsideEditor) { const now = new Date().getTime(); this.logLines = this.logLines.filter(e => e.ttl > now); diff --git a/tsconfig.json b/tsconfig.json index 0433089..e0da357 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,5 @@ "strictFunctionTypes": true }, "include": ["**/*.ts"], - "exclude": ["pouchdb-browser-webpack", "utils", "src/modules/coreObsidian/devUtil/tests.ts", "src/lib/src/API/**"] + "exclude": ["pouchdb-browser-webpack", "utils"] } diff --git a/updates.md b/updates.md index b0bd4b5..9323acb 100644 --- a/updates.md +++ b/updates.md @@ -1,97 +1,42 @@ -### 0.23.0 -Incredibly new features! +## 0.24.0 RC Release Note -Now, we can use object storage (MinIO, S3, R2 or anything you like) for synchronising! Moreover, despite that, we can use all the features as if we were using CouchDB. -Note: As this is a pretty experimental feature, hence we have some limitations. -- This is built on the append-only architecture. It will not shrink used storage if we do not perform a rebuild. -- A bit fragile. However, our version x.yy.0 is always so. -- When the first synchronisation, the entire history to date is transferred. For this reason, it is preferable to do this under the WiFi network. -- Do not worry, from the second synchronisation, we always transfer only differences. +**Note:** This will be rewritten with the stable release. I confess, before you take the time, this is quite long. -I hope this feature empowers users to maintain independence and self-host their data, offering an alternative for those who prefer to manage their own storage solutions and avoid being stuck on the right side of a sudden change in business model. +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. -Of course, I use Self-hosted MinIO for testing and recommend this. It is for the same reason as using CouchDB. -- open, controllable, auditable and indeed already audited by numerous eyes. +Consequently, this has led to a situation where bugs can go unnoticed or resolving one issue may inadvertently introduce another. -Let me write one more acknowledgement. +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). -I have a lot of respect for that plugin, even though it is sometimes treated as if it is a competitor, remotely-save. I think it is a great architecture that embodies a different approach to my approach of recreating history. This time, with all due respect, I have used some of its code as a reference. -Hooray for open source, and generous licences, and the sharing of knowledge by experts. +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. -#### Version history -- 0.23.23: - - Refined: - - Setting dialogue very slightly refined. - - The hodgepodge inside the `Hatch` pane has been sorted into more explicit categorised panes. - - Now we have new panes for: - - `Selector` - - `Advanced` - - `Power users` - - `Patches (Edge case)` - - Applying the settings will now be more informative. - - The header bar will be shown for applying the settings which needs a database rebuild. - - Applying methods are now more clearly navigated. - - Definitely, drastic change. I hope this will be more user-friendly. However, if you notice any issues, please let me know. I hope that nothing missed. - - New features: - - Word-segmented chunk building on users language - - Chunks can now be built with word-segmented data, enhancing efficiency for markdown files which contains the multiple sentences in a single line. - - This feature is enabled by default through `Use Segmented-splitter`. - - (Default: Disabled, Please be relived, I have learnt). - - Fixed: - - Sending chunks on `Send chunk in bulk` are now buffered to avoid the out-of-memory error. - - `Send chunk in bulk` is back to default disabled. (Sorry, not applied to the migrated users; I did not think we should deepen the wound any further "automatically"). - - Merging conflicts of JSON files are now works fine even if it contains `null`. - - Development: - - Implemented the logic for automatically generating the stub of document for the setting dialogue. -- 0.23.22: - - Fixed: - - Case-insensitive file handling - - Full-lower-case files are no longer created during database checking. - - Bulk chunk transfer - - The default value will automatically adjust to an acceptable size when using IBM Cloudant. -- 0.23.21: - - New Features: - - Case-insensitive file handling - - Files can now be handled case-insensitively. - - This behaviour can be modified in the settings under `Handle files as Case-Sensitive` (Default: Prompt, Enabled for previous behaviour). - - Improved chunk revision fixing - - Revisions for chunks can now be fixed for faster chunk creation. - - This can be adjusted in the settings under `Compute revisions for chunks` (Default: Prompt, Enabled for previous behaviour). - - Bulk chunk transfer - - Chunks can now be transferred in bulk during uploads. - - This feature is enabled by default through `Send chunks in bulk`. - - Creation of missing chunks without - - Missing chunks can be created without storing notes, enhancing efficiency for first synchronisation or after prolonged periods without synchronisation. - - Improvements: - - File status scanning on the startup - - Quite significant performance improvements. - - No more missing scans of some files. - - Status in editor enhancements - - Significant performance improvements in the status display within the editor. - - Notifications for files that will not be synchronised will now be properly communicated. - - Encryption and Decryption - - These processes are now performed in background threads to ensure fast and stable transfers. - - Verify and repair all files - - Got faster through parallel checking. - - Migration on update - - Migration messages and wizards have become more helpful. - - Behavioural changes: - - Chunk size adjustments - - Large chunks will no longer be created for older, stable files, addressing storage consumption issues. - - Flag file automation - - Confirmation will be shown and we can cancel it. - - Fixed: - - Database File Scanning - - All files in the database will now be enumerated correctly. - - Miscellaneous - - Dependency updated. - - Now, tree shaking is left to terser, from esbuild. -- 0.23.20: - - Fixed: - - Customisation Sync now checks the difference while storing or applying the configuration. - - No longer storing the same configuration multiple times. - - Time difference in the dialogue has been fixed. - - Remote Storage Limit Notification dialogue has been fixed, now the chosen value is saved. - - Improved: - - The Enlarging button on the enlarging threshold dialogue now displays the new value. +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. -Older notes is in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). \ No newline at end of file +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. + +Thank you, and I hope your troubles will be resolved! + +--- + +## 0.24.0.dev-rc2 + +### Fixed + +- Some status icons is now shown correctly. + +## 0.24.0-rc1 + +### Fixed + +- A fair numbers of bugs have been fixed. + +### Tiding + +- The codebase has been reorganised into clearly defined modules. + +Older notes is in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). diff --git a/updates_old.md b/updates_old.md index 91e9ea0..8961b01 100644 --- a/updates_old.md +++ b/updates_old.md @@ -18,6 +18,81 @@ I have a lot of respect for that plugin, even though it is sometimes treated as Hooray for open source, and generous licences, and the sharing of knowledge by experts. #### Version history +- 0.23.23: + - Refined: + - Setting dialogue very slightly refined. + - The hodgepodge inside the `Hatch` pane has been sorted into more explicit categorised panes. + - Now we have new panes for: + - `Selector` + - `Advanced` + - `Power users` + - `Patches (Edge case)` + - Applying the settings will now be more informative. + - The header bar will be shown for applying the settings which needs a database rebuild. + - Applying methods are now more clearly navigated. + - Definitely, drastic change. I hope this will be more user-friendly. However, if you notice any issues, please let me know. I hope that nothing missed. + - New features: + - Word-segmented chunk building on users language + - Chunks can now be built with word-segmented data, enhancing efficiency for markdown files which contains the multiple sentences in a single line. + - This feature is enabled by default through `Use Segmented-splitter`. + - (Default: Disabled, Please be relived, I have learnt). + - Fixed: + - Sending chunks on `Send chunk in bulk` are now buffered to avoid the out-of-memory error. + - `Send chunk in bulk` is back to default disabled. (Sorry, not applied to the migrated users; I did not think we should deepen the wound any further "automatically"). + - Merging conflicts of JSON files are now works fine even if it contains `null`. + - Development: + - Implemented the logic for automatically generating the stub of document for the setting dialogue. +- 0.23.22: + - Fixed: + - Case-insensitive file handling + - Full-lower-case files are no longer created during database checking. + - Bulk chunk transfer + - The default value will automatically adjust to an acceptable size when using IBM Cloudant. +- 0.23.21: + - New Features: + - Case-insensitive file handling + - Files can now be handled case-insensitively. + - This behaviour can be modified in the settings under `Handle files as Case-Sensitive` (Default: Prompt, Enabled for previous behaviour). + - Improved chunk revision fixing + - Revisions for chunks can now be fixed for faster chunk creation. + - This can be adjusted in the settings under `Compute revisions for chunks` (Default: Prompt, Enabled for previous behaviour). + - Bulk chunk transfer + - Chunks can now be transferred in bulk during uploads. + - This feature is enabled by default through `Send chunks in bulk`. + - Creation of missing chunks without + - Missing chunks can be created without storing notes, enhancing efficiency for first synchronisation or after prolonged periods without synchronisation. + - Improvements: + - File status scanning on the startup + - Quite significant performance improvements. + - No more missing scans of some files. + - Status in editor enhancements + - Significant performance improvements in the status display within the editor. + - Notifications for files that will not be synchronised will now be properly communicated. + - Encryption and Decryption + - These processes are now performed in background threads to ensure fast and stable transfers. + - Verify and repair all files + - Got faster through parallel checking. + - Migration on update + - Migration messages and wizards have become more helpful. + - Behavioural changes: + - Chunk size adjustments + - Large chunks will no longer be created for older, stable files, addressing storage consumption issues. + - Flag file automation + - Confirmation will be shown and we can cancel it. + - Fixed: + - Database File Scanning + - All files in the database will now be enumerated correctly. + - Miscellaneous + - Dependency updated. + - Now, tree shaking is left to terser, from esbuild. +- 0.23.20: + - Fixed: + - Customisation Sync now checks the difference while storing or applying the configuration. + - No longer storing the same configuration multiple times. + - Time difference in the dialogue has been fixed. + - Remote Storage Limit Notification dialogue has been fixed, now the chosen value is saved. + - Improved: + - The Enlarging button on the enlarging threshold dialogue now displays the new value. - 0.23.19: - Not released. - 0.23.18: From 7ca5ac5ac747a85b1b3ad0247a3b8b02306e4583 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 17 Oct 2024 10:19:08 +0100 Subject: [PATCH 3/8] 0.24.0.dev-rc3 --- manifest-beta.json | 2 +- package-lock.json | 4 +-- package.json | 2 +- src/features/ConfigSync/CmdConfigSync.ts | 32 +++++++++---------- .../HiddenFileSync/CmdHiddenFileSync.ts | 22 ++++++------- src/features/LiveSyncCommands.ts | 12 +++---- src/main.ts | 8 ++--- src/modules/AbstractObsidianModule.ts | 14 ++++---- src/modules/extras/ModuleDev.ts | 5 ++- updates.md | 7 ++++ 10 files changed, 59 insertions(+), 49 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index 8c4cc40..fee0d43 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.24.0.dev-rc2", + "version": "0.24.0.dev-rc3", "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", diff --git a/package-lock.json b/package-lock.json index 6369a8d..f218c14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc2", + "version": "0.24.0.dev-rc3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc2", + "version": "0.24.0.dev-rc3", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", diff --git a/package.json b/package.json index 9c8ef46..8db3c5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc2", + "version": "0.24.0.dev-rc3", "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", diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index 2c94712..2ea6ea7 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -343,7 +343,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { get useSyncPluginEtc() { return this.plugin.settings.usePluginEtc; } - $isThisModuleEnabled() { + _isThisModuleEnabled() { return this.plugin.settings.usePluginSync; } @@ -352,7 +352,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { pluginList: IPluginDataExDisplay[] = []; showPluginSyncModal() { - if (!this.$isThisModuleEnabled()) { + if (!this._isThisModuleEnabled()) { return; } if (this.pluginDialog) { @@ -417,7 +417,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return this.getFileCategory(filePath) != ""; } async $everyOnDatabaseInitialized(showNotice: boolean) { - if (!this.$isThisModuleEnabled()) return true; + if (!this._isThisModuleEnabled()) return true; try { Logger("Scanning customizations..."); await this.scanAllConfigFiles(showNotice); @@ -429,7 +429,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return true; } async $everyBeforeReplicate(showNotice: boolean) { - if (!this.$isThisModuleEnabled()) return true; + if (!this._isThisModuleEnabled()) return true; if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(showNotice); return true; @@ -437,8 +437,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return true; } async $everyOnResumeProcess(): Promise { - if (!this.$isThisModuleEnabled()) return true; - if (this.$isMainSuspended()) { + if (!this._isThisModuleEnabled()) return true; + if (this._isMainSuspended()) { return true; } if (this.settings.autoSweepPlugins) { @@ -449,7 +449,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } $everyAfterResumeProcess(): Promise { const q = activeDocument.querySelector(`.livesync-ribbon-showcustom`); - q?.toggleClass("sls-hidden", !this.$isThisModuleEnabled()); + q?.toggleClass("sls-hidden", !this._isThisModuleEnabled()); return Promise.resolve(true); } async reloadPluginList(showMessage: boolean) { @@ -754,7 +754,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise { - if (!this.$isThisModuleEnabled()) { + if (!this._isThisModuleEnabled()) { this.pluginScanProcessor.clearQueue(); this.pluginList = []; pluginList.set(this.pluginList) @@ -1019,10 +1019,10 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } async $anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument) { if (!docs._id.startsWith(ICXHeader)) return undefined; - if (this.$isThisModuleEnabled()) { + if (this._isThisModuleEnabled()) { await this.updatePluginList(false, (docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath((docs as AnyEntry))); } - if (this.$isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) { + if (this._isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) { if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) { const fragment = createFragment((doc) => { doc.createEl("span", undefined, (a) => { @@ -1063,9 +1063,9 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } async $everyRealizeSettingSyncMode(): Promise { this.periodicPluginSweepProcessor?.disable(); - if (!this.$isMainReady) return true; - if (!this.$isMainSuspended()) return true; - if (!this.$isThisModuleEnabled()) return true; + if (!this._isMainReady) return true; + if (!this._isMainSuspended()) return true; + if (!this._isThisModuleEnabled()) return true; if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(false); } @@ -1323,9 +1323,9 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } async watchVaultRawEventsAsync(path: FilePath) { - // if (!this.$isMainReady) return true; - // if (!this.$isMainSuspended()) return true; - if (!this.$isThisModuleEnabled()) return true; + if (!this._isMainReady) return true; + if (!this._isMainSuspended()) return true; + if (!this._isThisModuleEnabled()) return true; if (!this.isTargetPath(path)) return false; const stat = await this.plugin.storageAccess.statHidden(path); // Make sure that target is a file. diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index b86a088..5446036 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -14,11 +14,11 @@ import type { IObsidianModule } from "../../modules/AbstractObsidianModule.ts"; export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule { - $isThisModuleEnabled() { + _isThisModuleEnabled() { return this.plugin.settings.syncInternalFiles; } - periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(this.plugin, async () => this.$isThisModuleEnabled() && this.$isDatabaseReady() && await this.syncInternalFilesAndDatabase("push", false)); + periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(this.plugin, async () => this._isThisModuleEnabled() && this._isDatabaseReady() && await this.syncInternalFilesAndDatabase("push", false)); get kvDB() { return this.plugin.kvDB; @@ -39,7 +39,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule }); } async $everyOnDatabaseInitialized(showNotice: boolean) { - if (this.$isThisModuleEnabled()) { + if (this._isThisModuleEnabled()) { try { Logger("Synchronizing hidden files..."); await this.syncInternalFilesAndDatabase("push", showNotice); @@ -52,7 +52,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule return true; } async $everyBeforeReplicate(showNotice: boolean) { - if (this.$isThisModuleEnabled() && this.$isDatabaseReady() && this.settings.syncInternalFilesBeforeReplication && !this.settings.watchInternalFileChanges) { + if (this._isThisModuleEnabled() && this._isDatabaseReady() && this.settings.syncInternalFilesBeforeReplication && !this.settings.watchInternalFileChanges) { await this.syncInternalFilesAndDatabase("push", showNotice); } return true; @@ -61,22 +61,22 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule async $everyOnResumeProcess(): Promise { this.periodicInternalFileScanProcessor?.disable(); - if (this.$isMainSuspended()) + if (this._isMainSuspended()) return true; - if (this.$isThisModuleEnabled()) { + if (this._isThisModuleEnabled()) { await this.syncInternalFilesAndDatabase("safe", false); } - this.periodicInternalFileScanProcessor.enable(this.$isThisModuleEnabled() && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); + this.periodicInternalFileScanProcessor.enable(this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); return true } $everyRealizeSettingSyncMode(): Promise { this.periodicInternalFileScanProcessor?.disable(); - if (this.$isMainSuspended()) + if (this._isMainSuspended()) return Promise.resolve(true); if (!this.plugin.isReady) return Promise.resolve(true); - this.periodicInternalFileScanProcessor.enable(this.$isThisModuleEnabled() && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); + this.periodicInternalFileScanProcessor.enable(this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); return Promise.resolve(true); } @@ -98,7 +98,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule return await this.watchVaultRawEventsAsync(path); } async watchVaultRawEventsAsync(path: FilePath): Promise { - if (!this.$isThisModuleEnabled()) return false; + if (!this._isThisModuleEnabled()) return false; if (!isInternalMetadata(path)) return false; // Exclude files handled by customization sync @@ -274,7 +274,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } async $anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise { - if (isInternalMetadata(doc._id) && this.$isThisModuleEnabled()) { + if (isInternalMetadata(doc._id) && this._isThisModuleEnabled()) { //system file const filename = getPath(doc); if (await this.plugin.$$isTargetFile(filename)) { diff --git a/src/features/LiveSyncCommands.ts b/src/features/LiveSyncCommands.ts index 8ea24c4..d0e3fa3 100644 --- a/src/features/LiveSyncCommands.ts +++ b/src/features/LiveSyncCommands.ts @@ -31,13 +31,13 @@ export abstract class LiveSyncCommands { abstract onunload(): void; abstract onload(): void | Promise; - $isMainReady() { - return this.plugin.$isMainReady(); + _isMainReady() { + return this.plugin._isMainReady(); } - $isMainSuspended() { - return this.plugin.$isMainSuspended(); + _isMainSuspended() { + return this.plugin._isMainSuspended(); } - $isDatabaseReady() { - return this.plugin.$isDatabaseReady(); + _isDatabaseReady() { + return this.plugin._isDatabaseReady(); } } diff --git a/src/main.ts b/src/main.ts index 7f8e63e..b9366cb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -618,10 +618,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo $everyModuleTestMultiDevice(): Promise { 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; } + _isMainReady(): boolean { return this.isReady; } + _isMainSuspended(): boolean { return this.suspended; } + _isThisModuleEnabled(): boolean { return true; } + _isDatabaseReady(): boolean { return this.localDatabase.isReady; } $anyGetAppId(): Promise { return InterceptiveAny; } } diff --git a/src/modules/AbstractObsidianModule.ts b/src/modules/AbstractObsidianModule.ts index 6bef67d..60e9b3b 100644 --- a/src/modules/AbstractObsidianModule.ts +++ b/src/modules/AbstractObsidianModule.ts @@ -38,18 +38,18 @@ export abstract class AbstractObsidianModule extends AbstractModule { saveSettings = this.plugin.saveSettings.bind(this.plugin); - $isMainReady() { - return this.core.$isMainReady(); + _isMainReady() { + return this.core._isMainReady(); } - $isMainSuspended() { - return this.core.$isMainSuspended(); + _isMainSuspended() { + return this.core._isMainSuspended(); } - $isDatabaseReady() { - return this.core.$isDatabaseReady(); + _isDatabaseReady() { + return this.core._isDatabaseReady(); } //should be overridden - $isThisModuleEnabled() { + _isThisModuleEnabled() { return true } } \ No newline at end of file diff --git a/src/modules/extras/ModuleDev.ts b/src/modules/extras/ModuleDev.ts index bd7ed9b..42cb034 100644 --- a/src/modules/extras/ModuleDev.ts +++ b/src/modules/extras/ModuleDev.ts @@ -9,9 +9,12 @@ import { writable } from "svelte/store"; export class ModuleDev extends AbstractObsidianModule implements IObsidianModule { + $everyOnloadStart(): Promise { + __onMissingTranslation(() => { }); + return Promise.resolve(true); + } $everyOnloadAfterLoadSettings(): Promise { if (!this.settings.enableDebugTools) return Promise.resolve(true); - __onMissingTranslation(() => { }); // eslint-disable-next-line no-unused-labels __onMissingTranslation((key) => { const now = new Date(); diff --git a/updates.md b/updates.md index 9323acb..a286c07 100644 --- a/updates.md +++ b/updates.md @@ -23,6 +23,13 @@ Thank you, and I hope your troubles will be resolved! --- +## 0.24.0.dev-rc3 + +### Fixed + +- No longer Missing Translation Warning is shown in the console. +- Fixed the issue where some functions were not working properly (`_` started functions). + ## 0.24.0.dev-rc2 ### Fixed From e0e0ab04267d16684fa1df1cc71439853e7fd391 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 18 Oct 2024 11:14:58 +0100 Subject: [PATCH 4/8] 0.24.0.dev-rc4 --- manifest-beta.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/common/events.ts | 1 + src/features/ConfigSync/CmdConfigSync.ts | 14 +- .../HiddenFileSync/CmdHiddenFileSync.ts | 20 +- src/lib | 2 +- src/main.ts | 32 +- src/modules/core/ModuleFileHandler.ts | 14 +- src/modules/core/ModuleRebuilder.ts | 19 +- src/modules/core/ModuleReplicator.ts | 20 +- .../coreFeatures/ModuleCheckRemoteSize.ts | 71 +-- src/modules/coreFeatures/ModuleRedFlag.ts | 8 +- src/modules/coreObsidian/UILib/dialogs.ts | 54 +- src/modules/essential/ModuleMigration.ts | 98 ++-- .../ModuleInteractiveConflictResolver.ts | 22 - .../features/ModuleObsidianSettingTab.ts | 7 +- .../ObsidianLiveSyncSettingTab.ts | 494 ++++++++++-------- updates.md | 38 ++ 19 files changed, 555 insertions(+), 367 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index fee0d43..83884de 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.24.0.dev-rc3", + "version": "0.24.0.dev-rc4", "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", diff --git a/package-lock.json b/package-lock.json index f218c14..d46f8d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc3", + "version": "0.24.0.dev-rc4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc3", + "version": "0.24.0.dev-rc4", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", diff --git a/package.json b/package.json index 8db3c5b..30b229a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc3", + "version": "0.24.0.dev-rc4", "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", diff --git a/src/common/events.ts b/src/common/events.ts index 24930b7..97a1d1a 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -9,6 +9,7 @@ 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"; diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index 2ea6ea7..010570c 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -1492,14 +1492,13 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return true; } async _askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { - const message = `Would you like to enable \`Customization sync\`? -This feature allows you to sync your customisations -- such as configurations, themes, snippets, and plugins -- across your devices in a fully controlled manner, unlike the fully automatic behaviour of hidden file synchronisation. + const message = `Would you like to enable **Customization sync**? -You may use this feature alongside hidden file synchronisation. When both features are enabled, items configured as \`Automatic\` in this feature will be managed by hidden file synchronisation. - -Do not worry, you will be prompted to enable or keep disabled hidden file synchronisation after this dialogue. - -Of course, you can enable or disable this feature at any time. +> [!DETAILS]- +> This feature allows you to sync your customisations -- such as configurations, themes, snippets, and plugins -- across your devices in a fully controlled manner, unlike the fully automatic behaviour of hidden file synchronisation. +> +> You may use this feature alongside hidden file synchronisation. When both features are enabled, items configured as \`Automatic\` in this feature will be managed by **hidden file synchronisation**. +> Do not worry, you will be prompted to enable or keep disabled **hidden file synchronisation** after this dialogue. ` const CHOICE_CUSTOMIZE = "Yes, Enable it"; const CHOICE_DISABLE = "No, Disable it"; @@ -1576,6 +1575,7 @@ Of course, you can enable or disable this feature at any time. this.plugin.deviceAndVaultName = name; } this.plugin.settings.usePluginSync = true; + this.plugin.settings.useAdvancedMode = true; await this.plugin.saveSettings(); await this.scanAllConfigFiles(true); } diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index 5446036..e7ef060 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -748,18 +748,19 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule return true; } async _askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { - const messageFetch = `${opt.enableFetch ? `- Fetch: Use the files stored from other devices. Choose this option if you have already configured hidden file synchronization on those devices and wish to accept their files.\n` : ""}`; - const messageOverwrite = `${opt.enableOverwrite ? ` - Overwrite: Use the files from this device. Select this option if you want to overwrite the files stored on other devices.\n` : ""}`; - const messageMerge = `- Merge: Merge the files from this device with those on other devices. Choose this option if you wish to combine files from multiple sources. - However, please be reminded that merging may cause conflicts if the files are not identical. Additionally, this process may occur within the same folder, potentially breaking your plug-in or theme settings that comprise multiple files.\n`; - const message = `Would you like to enable \`Hidden File Synchronization\`? - -This feature allows you to synchronize all hidden files without any user interaction. -To enable this feature, you should choose one of the following options: + const messageFetch = `${opt.enableFetch ? `> - Fetch: Use the files stored from other devices. Choose this option if you have already configured hidden file synchronization on those devices and wish to accept their files.\n` : ""}`; + const messageOverwrite = `${opt.enableOverwrite ? `> - Overwrite: Use the files from this device. Select this option if you want to overwrite the files stored on other devices.\n` : ""}`; + const messageMerge = `> - Merge: Merge the files from this device with those on other devices. Choose this option if you wish to combine files from multiple sources. +> However, please be reminded that merging may cause conflicts if the files are not identical. Additionally, this process may occur within the same folder, potentially breaking your plug-in or theme settings that comprise multiple files.\n`; + const message = `Would you like to enable **Hidden File Synchronization**? +> [!DETAILS]- +> This feature allows you to synchronize all hidden files without any user interaction. +> To enable this feature, you should choose one of the following options: ${messageFetch}${messageOverwrite}${messageMerge} -Note: Please keep in mind that enabling this feature alongside customisation sync may override certain behaviors.` +> [!IMPORTANT] +> Please keep in mind that enabling this feature alongside customisation sync may override certain behaviors.` const CHOICE_FETCH = "Fetch"; const CHOICE_OVERWRITE = "Overwrite"; const CHOICE_MERGE = "Merge"; @@ -817,6 +818,7 @@ Note: Please keep in mind that enabling this feature alongside customisation syn } else if (mode == "MERGE") { await this.syncInternalFilesAndDatabase("safe", true); } + this.plugin.settings.useAdvancedMode = true; this.plugin.settings.syncInternalFiles = true; await this.plugin.saveSettings(); Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE); diff --git a/src/lib b/src/lib index 92d7b03..5079b0b 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 92d7b03916cce550fba4fc2334d7eb4d57960e00 +Subproject commit 5079b0bf79d94495beaed174e9a71dbfa537f7c4 diff --git a/src/main.ts b/src/main.ts index b9366cb..1f5cf13 100644 --- a/src/main.ts +++ b/src/main.ts @@ -347,11 +347,29 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo 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); + 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.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.$$scheduleAppReload(); + return; + } } const isInitialized = await this.$$initializeDatabase(false, false); if (!isInitialized) { @@ -411,7 +429,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo 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); + Logger($f`You have some unread release notes! Please read them once!`, LOG_LEVEL_NOTICE); } //@ts-ignore @@ -559,7 +577,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo $anyAfterConnectCheckFailed(): Promise { return InterceptiveAny; } - $$replicateAllToServer(showingNotice: boolean = false): Promise { throwShouldBeOverridden() } + $$replicateAllToServer(showingNotice: boolean = false, sendChunksInBulkDisabled: boolean = false): Promise { throwShouldBeOverridden() } $$replicateAllFromServer(showingNotice: boolean = false): Promise { throwShouldBeOverridden() } // Remote Governing diff --git a/src/modules/core/ModuleFileHandler.ts b/src/modules/core/ModuleFileHandler.ts index ff0bd33..424072d 100644 --- a/src/modules/core/ModuleFileHandler.ts +++ b/src/modules/core/ModuleFileHandler.ts @@ -54,21 +54,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. diff --git a/src/modules/core/ModuleRebuilder.ts b/src/modules/core/ModuleRebuilder.ts index e7784d1..4652802 100644 --- a/src/modules/core/ModuleRebuilder.ts +++ b/src/modules/core/ModuleRebuilder.ts @@ -27,6 +27,15 @@ 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; @@ -36,11 +45,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); } $rebuildRemote(): Promise { return this.rebuildRemote(); @@ -59,11 +68,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); } @@ -169,7 +178,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); diff --git a/src/modules/core/ModuleReplicator.ts b/src/modules/core/ModuleReplicator.ts index db1551e..c06ecf4 100644 --- a/src/modules/core/ModuleReplicator.ts +++ b/src/modules/core/ModuleReplicator.ts @@ -231,12 +231,17 @@ Or if you are sure know what had been happened, we can unlock the database from 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: ${change.path}`, LOG_LEVEL_INFO); + Logger(`Processing scheduled: ${docPath}`, 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); + Logger(`Processing ${docPath} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE); return; } this.databaseQueuedProcessor.enqueue(change); @@ -258,6 +263,7 @@ Or if you are sure know what had been happened, we can unlock the database from 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); @@ -308,15 +314,17 @@ Or if you are sure know what had been happened, we can unlock the database from return Promise.resolve(true); } - async $$replicateAllToServer(showingNotice: boolean = false): Promise { + async $$replicateAllToServer(showingNotice: boolean = false, sendChunksInBulkDisabled: boolean = false): Promise { 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); diff --git a/src/modules/coreFeatures/ModuleCheckRemoteSize.ts b/src/modules/coreFeatures/ModuleCheckRemoteSize.ts index 3a5dd8d..9435544 100644 --- a/src/modules/coreFeatures/ModuleCheckRemoteSize.ts +++ b/src/modules/coreFeatures/ModuleCheckRemoteSize.ts @@ -1,39 +1,40 @@ 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 { 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. - -- 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). +If we have reached the limit, we will be asked to enlarge the limit step by step. ` - const ANSWER_0 = "Do not warn"; - const ANSWER_800 = "800MB"; - const ANSWER_2000 = "2GB"; + 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"; - 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,28 +46,38 @@ 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" }); 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) { diff --git a/src/modules/coreFeatures/ModuleRedFlag.ts b/src/modules/coreFeatures/ModuleRedFlag.ts index 84dd560..2d6d26b 100644 --- a/src/modules/coreFeatures/ModuleRedFlag.ts +++ b/src/modules/coreFeatures/ModuleRedFlag.ts @@ -77,7 +77,11 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule { } } 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"); + 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) { @@ -87,6 +91,8 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule { 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. diff --git a/src/modules/coreObsidian/UILib/dialogs.ts b/src/modules/coreObsidian/UILib/dialogs.ts index 5ce140d..6c43645 100644 --- a/src/modules/coreObsidian/UILib/dialogs.ts +++ b/src/modules/coreObsidian/UILib/dialogs.ts @@ -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.on(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; @@ -47,6 +69,7 @@ export class InputStringDialog extends Modal { } onClose() { + super.onClose(); const { contentEl } = this; contentEl.empty(); if (this.isManuallyClosed) { @@ -95,7 +118,7 @@ export class PopoverSelectString extends FuzzySuggestModal { } } -export class MessageBox extends Modal { +export class MessageBox extends AutoClosableModal { plugin: Plugin; title: string; @@ -144,16 +167,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,6 +188,15 @@ 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 @@ -190,6 +225,7 @@ export class MessageBox extends Modal { } onClose() { + super.onClose(); const { contentEl } = this; contentEl.empty(); if (this.timer) { diff --git a/src/modules/essential/ModuleMigration.ts b/src/modules/essential/ModuleMigration.ts index 472dc3f..701e1c7 100644 --- a/src/modules/essential/ModuleMigration.ts +++ b/src/modules/essential/ModuleMigration.ts @@ -1,9 +1,11 @@ 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 { 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"; +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() { @@ -159,6 +161,63 @@ ___However, to enable either of these changes, both remote and local databases n } + 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 { if (!this.localDatabase.isReady) { this._log(`Something went wrong! The local database is not ready`, LOG_LEVEL_NOTICE); @@ -170,40 +229,11 @@ ___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; } diff --git a/src/modules/features/ModuleInteractiveConflictResolver.ts b/src/modules/features/ModuleInteractiveConflictResolver.ts index 47670bb..dbfc4b1 100644 --- a/src/modules/features/ModuleInteractiveConflictResolver.ts +++ b/src/modules/features/ModuleInteractiveConflictResolver.ts @@ -106,28 +106,6 @@ 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(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(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 { const notes: { path: string, mtime: number }[] = []; this._log(`Checking conflicted files`, LOG_LEVEL_VERBOSE); diff --git a/src/modules/features/ModuleObsidianSettingTab.ts b/src/modules/features/ModuleObsidianSettingTab.ts index ece6987..b095760 100644 --- a/src/modules/features/ModuleObsidianSettingTab.ts +++ b/src/modules/features/ModuleObsidianSettingTab.ts @@ -1,7 +1,7 @@ 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 { @@ -11,6 +11,11 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule implem 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); } diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index a8e4ed4..8995f5d 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -405,11 +405,51 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { } } + screenElements: { [key: string]: HTMLElement[] } = {}; + changeDisplay(screen: string) { + for (const k in this.screenElements) { + if (k == screen) { + this.screenElements[k].forEach((element) => element.removeClass("setting-collapsed")); + } else { + this.screenElements[k].forEach((element) => element.addClass("setting-collapsed")); + } + } + if (this.menuEl) { + this.menuEl.querySelectorAll(`.sls-setting-label`).forEach((element) => { + if (element.hasClass(`c-${screen}`)) { + element.addClass("selected"); + (element.querySelector("input[type=radio]"))!.checked = true; + } else { + element.removeClass("selected"); + (element.querySelector("input[type=radio]"))!.checked = false; + } + }); + } + this.selectedScreen = screen; + } + async enableMinimalSetup() { + this.editingSettings.liveSync = false; + this.editingSettings.periodicReplication = false; + this.editingSettings.syncOnSave = false; + this.editingSettings.syncOnEditorSave = false; + this.editingSettings.syncOnStart = false; + this.editingSettings.syncOnFileOpen = false; + this.editingSettings.syncAfterMerge = false; + this.plugin.replicator.closeReplication(); + await this.saveAllDirtySettings(); + this.containerEl.addClass("isWizard"); + this.inWizard = true; + this.changeDisplay("20") + } + menuEl?: HTMLElement; + display(): void { + const changeDisplay = this.changeDisplay.bind(this); const { containerEl } = this; this.settingComponents.length = 0; this.controlledElementFunc.length = 0; this.onSavedHandlers.length = 0; + this.screenElements = {}; if (this._editingSettings == undefined || this.initialSettings == undefined) { this.reloadAllSettings(); } @@ -436,26 +476,26 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { setStyle(containerEl, "menu-setting-advanced", () => this.isConfiguredAs("useAdvancedMode", true)); setStyle(containerEl, "menu-setting-edgecase", () => this.isConfiguredAs("useEdgeCaseMode", true)); - const screenElements: { [key: string]: HTMLElement[] } = {}; const addScreenElement = (key: string, element: HTMLElement) => { - if (!(key in screenElements)) { - screenElements[key] = []; + if (!(key in this.screenElements)) { + this.screenElements[key] = []; } - screenElements[key].push(element); + this.screenElements[key].push(element); }; const menuWrapper = this.createEl(containerEl, "div", { cls: "sls-setting-menu-wrapper" }); - const w = menuWrapper.createDiv(""); - w.addClass("sls-setting-menu"); - const menuTabs = w.querySelectorAll(".sls-setting-label"); + if (this.menuEl) { + this.menuEl.remove(); + } + this.menuEl = menuWrapper.createDiv(""); + this.menuEl.addClass("sls-setting-menu"); + const menuTabs = this.menuEl.querySelectorAll(".sls-setting-label"); const selectPane = (event: Event) => { const target = event.target as HTMLElement; if (target.tagName == "INPUT") { const value = target.getAttribute("value"); if (value && this.selectedScreen != value) { changeDisplay(value); - // target.parentElement?.parentElement?.querySelector(".sls-setting-label.selected")?.removeClass("selected"); - // target.parentElement?.addClass("selected"); } } } @@ -575,17 +615,19 @@ Store only the settings. **Caution: This may lead to data corruption**; database } setLevelClass(el, level) el.createEl("h3", { text: title, cls: "sls-setting-pane-title" }); - w.createEl("label", { cls: `sls-setting-label c-${order} ${wizardHidden ? "wizardHidden" : ""}` }, el => { - setLevelClass(el, level) - const inputEl = el.createEl("input", { - type: "radio", name: "disp", value: `${order}`, cls: "sls-setting-tab" - } as DomElementInfo); - el.createEl("div", { - cls: "sls-setting-menu-btn", text: icon, title: title - }); - inputEl.addEventListener("change", selectPane); - inputEl.addEventListener("click", selectPane); - }) + if (this.menuEl) { + this.menuEl.createEl("label", { cls: `sls-setting-label c-${order} ${wizardHidden ? "wizardHidden" : ""}` }, el => { + setLevelClass(el, level) + const inputEl = el.createEl("input", { + type: "radio", name: "disp", value: `${order}`, cls: "sls-setting-tab" + } as DomElementInfo); + el.createEl("div", { + cls: "sls-setting-menu-btn", text: icon, title: title + }); + inputEl.addEventListener("change", selectPane); + inputEl.addEventListener("click", selectPane); + }) + } addScreenElement(`${order}`, el); const p = Promise.resolve(el) // fireAndForget @@ -615,31 +657,13 @@ Store only the settings. **Caution: This may lead to data corruption**; database return p; } - const changeDisplay = (screen: string) => { - for (const k in screenElements) { - if (k == screen) { - screenElements[k].forEach((element) => element.removeClass("setting-collapsed")); - } else { - screenElements[k].forEach((element) => element.addClass("setting-collapsed")); - } - } - w.querySelectorAll(`.sls-setting-label`).forEach((element) => { - if (element.hasClass(`c-${screen}`)) { - element.addClass("selected"); - (element.querySelector("input[type=radio]"))!.checked = true; - } else { - element.removeClass("selected"); - (element.querySelector("input[type=radio]"))!.checked = false; - } - }); - this.selectedScreen = screen; - }; + menuTabs.forEach((element) => { const e = element.querySelector(".sls-setting-tab"); if (!e) return; e.addEventListener("change", (event) => { menuTabs.forEach((element) => element.removeClass("selected")); - changeDisplay((event.currentTarget as HTMLInputElement).value); + this.changeDisplay((event.currentTarget as HTMLInputElement).value); element.addClass("selected"); }); }); @@ -772,18 +796,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database .setName("Minimal setup") .addButton((text) => { text.setButtonText("Start").onClick(async () => { - this.editingSettings.liveSync = false; - this.editingSettings.periodicReplication = false; - this.editingSettings.syncOnSave = false; - this.editingSettings.syncOnEditorSave = false; - this.editingSettings.syncOnStart = false; - this.editingSettings.syncOnFileOpen = false; - this.editingSettings.syncAfterMerge = false; - this.plugin.replicator.closeReplication(); - await this.saveAllDirtySettings(); - containerEl.addClass("isWizard"); - this.inWizard = true; - changeDisplay("0") + await this.enableMinimalSetup(); }) }) new Setting(paneEl) @@ -816,6 +829,8 @@ Store only the settings. **Caution: This may lead to data corruption**; database text.setButtonText("Discard").onClick(async () => { if (await this.plugin.confirm.askYesNoDialog("Do you really want to discard existing settings and databases?", { defaultOption: "No" }) == "yes") { this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS }; + await this.saveAllDirtySettings(); + this.plugin.settings = { ...DEFAULT_SETTINGS }; await this.plugin.$$saveSettingData(); await this.plugin.$$resetLocalDatabase(); // await this.plugin.initializeDatabase(); @@ -918,14 +933,193 @@ Store only the settings. **Caution: This may lead to data corruption**; database new Setting(paneEl).autoWireToggle("showStatusOnStatusbar"); }); void addPanel(paneEl, "Logging").then((paneEl) => { + paneEl.addClass("wizardHidden"); new Setting(paneEl).autoWireToggle("lessInformationInLog"); new Setting(paneEl) .autoWireToggle("showVerboseLog", { onUpdate: visibleOnly(() => this.isConfiguredAs("lessInformationInLog", false)) }); }); - + new Setting(paneEl) + .setClass("wizardOnly") + .addButton((button) => button + .setButtonText("Next") + .setCta() + .onClick(() => { + this.changeDisplay("0"); + }) + ); }) + let checkResultDiv: HTMLDivElement; + const checkConfig = async (checkResultDiv: HTMLDivElement | undefined) => { + Logger(`Checking database configuration`, LOG_LEVEL_INFO); + let isSuccessful = true; + const emptyDiv = createDiv(); + emptyDiv.innerHTML = ""; + checkResultDiv?.replaceChildren(...[emptyDiv]); + const addResult = (msg: string, classes?: string[]) => { + const tmpDiv = createDiv(); + tmpDiv.addClass("ob-btn-config-fix"); + if (classes) { + tmpDiv.addClasses(classes); + } + tmpDiv.innerHTML = `${msg}`; + checkResultDiv?.appendChild(tmpDiv); + }; + try { + + if (isCloudantURI(this.editingSettings.couchDB_URI)) { + Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL_NOTICE); + return; + } + const r = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, window.origin); + const responseConfig = r.json; + + const addConfigFixButton = (title: string, key: string, value: string) => { + if (!checkResultDiv) return; + const tmpDiv = createDiv(); + tmpDiv.addClass("ob-btn-config-fix"); + tmpDiv.innerHTML = ``; + const x = checkResultDiv.appendChild(tmpDiv); + x.querySelector("button")?.addEventListener("click", () => { + fireAndForget(async () => { + Logger(`CouchDB Configuration: ${title} -> Set ${key} to ${value}`) + const res = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, undefined, key, value); + if (res.status == 200) { + Logger(`CouchDB Configuration: ${title} successfully updated`, LOG_LEVEL_NOTICE); + checkResultDiv.removeChild(x); + await checkConfig(checkResultDiv); + } else { + Logger(`CouchDB Configuration: ${title} failed`, LOG_LEVEL_NOTICE); + Logger(res.text, LOG_LEVEL_VERBOSE); + } + }); + }); + }; + addResult("---Notice---", ["ob-btn-config-head"]); + addResult("If the server configuration is not persistent (e.g., running on docker), the values set from here will also be volatile. Once you are able to connect, please reflect the settings in the server's local.ini.", ["ob-btn-config-info"]); + + addResult("--Config check--", ["ob-btn-config-head"]); + + // Admin check + // for database creation and deletion + if (!(this.editingSettings.couchDB_USER in responseConfig.admins)) { + addResult(`⚠ You do not have administrative privileges.`); + } else { + addResult("✔ You have administrative privileges."); + } + // HTTP user-authorization check + if (responseConfig?.chttpd?.require_valid_user != "true") { + isSuccessful = false; + addResult("❗ chttpd.require_valid_user is wrong."); + addConfigFixButton("Set chttpd.require_valid_user = true", "chttpd/require_valid_user", "true"); + } else { + addResult("✔ chttpd.require_valid_user is ok."); + } + if (responseConfig?.chttpd_auth?.require_valid_user != "true") { + isSuccessful = false; + addResult("❗ chttpd_auth.require_valid_user is wrong."); + addConfigFixButton("Set chttpd_auth.require_valid_user = true", "chttpd_auth/require_valid_user", "true"); + } else { + addResult("✔ chttpd_auth.require_valid_user is ok."); + } + // HTTPD check + // Check Authentication header + if (!responseConfig?.httpd["WWW-Authenticate"]) { + isSuccessful = false; + addResult("❗ httpd.WWW-Authenticate is missing"); + addConfigFixButton("Set httpd.WWW-Authenticate", "httpd/WWW-Authenticate", 'Basic realm="couchdb"'); + } else { + addResult("✔ httpd.WWW-Authenticate is ok."); + } + if (responseConfig?.httpd?.enable_cors != "true") { + isSuccessful = false; + addResult("❗ httpd.enable_cors is wrong"); + addConfigFixButton("Set httpd.enable_cors", "httpd/enable_cors", "true"); + } else { + addResult("✔ httpd.enable_cors is ok."); + } + // If the server is not cloudant, configure request size + if (!isCloudantURI(this.editingSettings.couchDB_URI)) { + // REQUEST SIZE + if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) { + isSuccessful = false; + addResult("❗ chttpd.max_http_request_size is low)"); + addConfigFixButton("Set chttpd.max_http_request_size", "chttpd/max_http_request_size", "4294967296"); + } else { + addResult("✔ chttpd.max_http_request_size is ok."); + } + if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) { + isSuccessful = false; + addResult("❗ couchdb.max_document_size is low)"); + addConfigFixButton("Set couchdb.max_document_size", "couchdb/max_document_size", "50000000"); + } else { + addResult("✔ couchdb.max_document_size is ok."); + } + } + // CORS check + // checking connectivity for mobile + if (responseConfig?.cors?.credentials != "true") { + isSuccessful = false; + addResult("❗ cors.credentials is wrong"); + addConfigFixButton("Set cors.credentials", "cors/credentials", "true"); + } else { + addResult("✔ cors.credentials is ok."); + } + const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(","); + if (responseConfig?.cors?.origins == "*" || (ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 && ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && ConfiguredOrigins.indexOf("http://localhost") !== -1)) { + addResult("✔ cors.origins is ok."); + } else { + addResult("❗ cors.origins is wrong"); + addConfigFixButton("Set cors.origins", "cors/origins", "app://obsidian.md,capacitor://localhost,http://localhost"); + isSuccessful = false; + } + addResult("--Connection check--", ["ob-btn-config-head"]); + addResult(`Current origin:${window.location.origin}`); + + // Request header check + const origins = [ + "app://obsidian.md", + "capacitor://localhost", + "http://localhost"]; + for (const org of origins) { + const rr = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, org); + const responseHeaders = Object.fromEntries(Object.entries(rr.headers) + .map((e) => { + e[0] = `${e[0]}`.toLowerCase(); + return e; + })); + addResult(`Origin check:${org}`); + if (responseHeaders["access-control-allow-credentials"] != "true") { + addResult("❗ CORS is not allowing credentials"); + isSuccessful = false; + } else { + addResult("✔ CORS credentials OK"); + } + if (responseHeaders["access-control-allow-origin"] != org) { + addResult(`⚠ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}`); + } else { + addResult("✔ CORS origin OK"); + } + } + addResult("--Done--", ["ob-btn-config-head"]); + addResult("If you have some trouble with Connection-check even though all Config-check has been passed, please check your reverse proxy's configuration.", ["ob-btn-config-info"]); + Logger(`Checking configuration done`, LOG_LEVEL_INFO); + } catch (ex: any) { + if (ex?.status == 401) { + isSuccessful = false; + addResult(`❗ Access forbidden.`); + addResult(`We could not continue the test.`); + Logger(`Checking configuration done`, LOG_LEVEL_INFO); + } else { + Logger(`Checking configuration failed`, LOG_LEVEL_NOTICE); + Logger(ex); + isSuccessful = false; + } + } + return isSuccessful; + }; + void addPane(containerEl, "Remote Configuration", "🛰️", 0, false).then((paneEl) => { void addPanel(paneEl, "Remote Server").then((paneEl) => { // const containerRemoteDatabaseEl = containerEl.createDiv(); @@ -1039,164 +1233,9 @@ However, your report is needed to stabilise this. I appreciate you for your grea .setButtonText("Check") .setDisabled(false) .onClick(async () => { - const checkConfig = async () => { - Logger(`Checking database configuration`, LOG_LEVEL_INFO); - - const emptyDiv = createDiv(); - emptyDiv.innerHTML = ""; - checkResultDiv.replaceChildren(...[emptyDiv]); - const addResult = (msg: string, classes?: string[]) => { - const tmpDiv = createDiv(); - tmpDiv.addClass("ob-btn-config-fix"); - if (classes) { - tmpDiv.addClasses(classes); - } - tmpDiv.innerHTML = `${msg}`; - checkResultDiv.appendChild(tmpDiv); - }; - try { - - if (isCloudantURI(this.editingSettings.couchDB_URI)) { - Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL_NOTICE); - return; - } - const r = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, window.origin); - const responseConfig = r.json; - - const addConfigFixButton = (title: string, key: string, value: string) => { - const tmpDiv = createDiv(); - tmpDiv.addClass("ob-btn-config-fix"); - tmpDiv.innerHTML = ``; - const x = checkResultDiv.appendChild(tmpDiv); - x.querySelector("button")?.addEventListener("click", () => { - fireAndForget(async () => { - Logger(`CouchDB Configuration: ${title} -> Set ${key} to ${value}`) - const res = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, undefined, key, value); - if (res.status == 200) { - Logger(`CouchDB Configuration: ${title} successfully updated`, LOG_LEVEL_NOTICE); - checkResultDiv.removeChild(x); - await checkConfig(); - } else { - Logger(`CouchDB Configuration: ${title} failed`, LOG_LEVEL_NOTICE); - Logger(res.text, LOG_LEVEL_VERBOSE); - } - }); - }); - }; - addResult("---Notice---", ["ob-btn-config-head"]); - addResult("If the server configuration is not persistent (e.g., running on docker), the values set from here will also be volatile. Once you are able to connect, please reflect the settings in the server's local.ini.", ["ob-btn-config-info"]); - - addResult("--Config check--", ["ob-btn-config-head"]); - - // Admin check - // for database creation and deletion - if (!(this.editingSettings.couchDB_USER in responseConfig.admins)) { - addResult(`⚠ You do not have administrative privileges.`); - } else { - addResult("✔ You have administrative privileges."); - } - // HTTP user-authorization check - if (responseConfig?.chttpd?.require_valid_user != "true") { - addResult("❗ chttpd.require_valid_user is wrong."); - addConfigFixButton("Set chttpd.require_valid_user = true", "chttpd/require_valid_user", "true"); - } else { - addResult("✔ chttpd.require_valid_user is ok."); - } - if (responseConfig?.chttpd_auth?.require_valid_user != "true") { - addResult("❗ chttpd_auth.require_valid_user is wrong."); - addConfigFixButton("Set chttpd_auth.require_valid_user = true", "chttpd_auth/require_valid_user", "true"); - } else { - addResult("✔ chttpd_auth.require_valid_user is ok."); - } - // HTTPD check - // Check Authentication header - if (!responseConfig?.httpd["WWW-Authenticate"]) { - addResult("❗ httpd.WWW-Authenticate is missing"); - addConfigFixButton("Set httpd.WWW-Authenticate", "httpd/WWW-Authenticate", 'Basic realm="couchdb"'); - } else { - addResult("✔ httpd.WWW-Authenticate is ok."); - } - if (responseConfig?.httpd?.enable_cors != "true") { - addResult("❗ httpd.enable_cors is wrong"); - addConfigFixButton("Set httpd.enable_cors", "httpd/enable_cors", "true"); - } else { - addResult("✔ httpd.enable_cors is ok."); - } - // If the server is not cloudant, configure request size - if (!isCloudantURI(this.editingSettings.couchDB_URI)) { - // REQUEST SIZE - if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) { - addResult("❗ chttpd.max_http_request_size is low)"); - addConfigFixButton("Set chttpd.max_http_request_size", "chttpd/max_http_request_size", "4294967296"); - } else { - addResult("✔ chttpd.max_http_request_size is ok."); - } - if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) { - addResult("❗ couchdb.max_document_size is low)"); - addConfigFixButton("Set couchdb.max_document_size", "couchdb/max_document_size", "50000000"); - } else { - addResult("✔ couchdb.max_document_size is ok."); - } - } - // CORS check - // checking connectivity for mobile - if (responseConfig?.cors?.credentials != "true") { - addResult("❗ cors.credentials is wrong"); - addConfigFixButton("Set cors.credentials", "cors/credentials", "true"); - } else { - addResult("✔ cors.credentials is ok."); - } - const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(","); - if (responseConfig?.cors?.origins == "*" || (ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 && ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && ConfiguredOrigins.indexOf("http://localhost") !== -1)) { - addResult("✔ cors.origins is ok."); - } else { - addResult("❗ cors.origins is wrong"); - addConfigFixButton("Set cors.origins", "cors/origins", "app://obsidian.md,capacitor://localhost,http://localhost"); - } - addResult("--Connection check--", ["ob-btn-config-head"]); - addResult(`Current origin:${window.location.origin}`); - - // Request header check - const origins = [ - "app://obsidian.md", - "capacitor://localhost", - "http://localhost"]; - for (const org of origins) { - const rr = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, org); - const responseHeaders = Object.fromEntries(Object.entries(rr.headers) - .map((e) => { - e[0] = `${e[0]}`.toLowerCase(); - return e; - })); - addResult(`Origin check:${org}`); - if (responseHeaders["access-control-allow-credentials"] != "true") { - addResult("❗ CORS is not allowing credentials"); - } else { - addResult("✔ CORS credentials OK"); - } - if (responseHeaders["access-control-allow-origin"] != org) { - addResult(`❗ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}`); - } else { - addResult("✔ CORS origin OK"); - } - } - addResult("--Done--", ["ob-btn-config-head"]); - addResult("If you have some trouble with Connection-check even though all Config-check has been passed, please check your reverse proxy's configuration.", ["ob-btn-config-info"]); - Logger(`Checking configuration done`, LOG_LEVEL_INFO); - } catch (ex: any) { - if (ex?.status == 401) { - addResult(`❗ Access forbidden.`); - addResult(`We could not continue the test.`); - Logger(`Checking configuration done`, LOG_LEVEL_INFO); - } else { - Logger(`Checking configuration failed`, LOG_LEVEL_NOTICE); - Logger(ex); - } - } - }; - await checkConfig(); + await checkConfig(checkResultDiv); })); - const checkResultDiv = this.createEl(paneEl, "div", { + checkResultDiv = this.createEl(paneEl, "div", { text: "", }); @@ -1245,10 +1284,26 @@ However, your report is needed to stabilise this. I appreciate you for your grea .setButtonText("Next") .setCta() .setDisabled(false) - .onClick(() => { + .onClick(async () => { + if (!await checkConfig(checkResultDiv)) { + if (await this.plugin.confirm.askYesNoDialog("The configuration check has failed. Do you want to continue anyway?", { defaultOption: "No", title: "Remote Configuration Check Failed" }) == "no") { + return; + } + } + const isEncryptionFullyEnabled = !this.editingSettings.encrypt || !this.editingSettings.usePathObfuscation; + if (isEncryptionFullyEnabled) { + if (await this.plugin.confirm.askYesNoDialog("Enabling End-to-End Encryption and Path Obfuscation is strongly recommended. Do you surely want to continue without encryption?", { defaultOption: "No", title: "Encryption is not enabled" }) == "no") { + return; + } + } if (!this.editingSettings.encrypt) { this.editingSettings.passphrase = ""; } + if (!await isPassphraseValid()) { + if (await this.plugin.confirm.askYesNoDialog("End-to-End encryption seems to have trouble. Do you surely want to continue with the current settings?", { defaultOption: "No", title: "Encryption has some trouble" }) == "no") { + return; + } + } if (isCloudantURI(this.editingSettings.couchDB_URI)) { this.editingSettings = { ...this.editingSettings, ...PREFERRED_SETTING_CLOUDANT }; } else if (this.editingSettings.remoteType == REMOTE_MINIO) { @@ -1279,7 +1334,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea } this.createEl(paneEl, "div", { - text: `Please select any preset to complete the wizard.`, + text: `Please select and apply any preset item to complete the wizard.`, cls: "wizardOnly" }).addClasses(["op-warn-info"]); @@ -1362,9 +1417,12 @@ However, your report is needed to stabilise this. I appreciate you for your grea await this.plugin.realizeSettingSyncMode(); await rebuildDB("localOnly"); // this.resetEditingSettings(); - Logger("All done! Please set up subsequent devices with 'Copy current settings as a new setup URI' and 'Use the copied setup URI'.", LOG_LEVEL_NOTICE); - // await this.plugin.addOnSetup.command_copySetupURI(); - eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); + if (await this.plugin.confirm.askYesNoDialog( + "All done!, do you want to generate a setup URI to set up other devices?", + { defaultOption: "Yes", title: "Congratulations!" } + ) == "yes") { + eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); + } } else { if (isNeedRebuildLocal() || isNeedRebuildRemote()) { await confirmRebuild(); @@ -1714,7 +1772,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea }); - void addPane(containerEl, "Hatch", "🧰", 50, false).then((paneEl) => { + void addPane(containerEl, "Hatch", "🧰", 50, true).then((paneEl) => { // const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` }); // hatchWarn.addClass("op-warn-info"); void addPanel(paneEl, "Reporting Issue").then((paneEl) => { @@ -2269,7 +2327,7 @@ ${stringifyYaml(pluginConfig)}`; const isRemoteLocked = () => this.plugin?.replicator?.remoteLocked; // if (this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted) { this.createEl(paneEl, "div", { - text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. It caused by some operations like this. Re-initialized. Local database initialization should be required. Please back your vault up, reset the local database, and press 'Mark this device as resolved'. ", + text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. It caused by some operations like this. Re-initialized. Local database initialization should be required. Please back your vault up, reset the local database, and press 'Mark this device as resolved'. This warning kept showing until confirming the device is resolved by the replication.", cls: "op-warn" }, c => { this.createEl(c, "button", { @@ -2284,7 +2342,7 @@ ${stringifyYaml(pluginConfig)}`; }) }, visibleOnly(isRemoteLockedAndDeviceNotAccepted)); this.createEl(paneEl, "div", { - text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database.", + text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database. This warning kept showing until confirming the device is resolved by the replication", cls: "op-warn" }, c => this.createEl(c, "button", { text: "I'm ready, unlock the database", cls: "mod-warning" diff --git a/updates.md b/updates.md index a286c07..8c7847a 100644 --- a/updates.md +++ b/updates.md @@ -23,6 +23,44 @@ Thank you, and I hope your troubles will be resolved! --- +## 0.24.0.dev-rc4 + +### Improved + +- The welcome message is now more simple to encourage the use of the Setup-URI. + - And 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 robust, 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. + +### Fixed + +- 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. + +### Changed + +- Some default settings have been changed for easier new user experience. + - Preventing the meaningless migration of the settings. + +### Tidied + +- Commented-out codes have been gradually removed. + ## 0.24.0.dev-rc3 ### Fixed From 6d244a6e345a86a80b578526032972f90c647827 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 21 Oct 2024 09:47:09 +0100 Subject: [PATCH 5/8] 0.24.0.dev-rc5 --- manifest-beta.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/features/ConfigSync/CmdConfigSync.ts | 186 +++++---- .../HiddenFileSync/CmdHiddenFileSync.ts | 377 ++++++++++-------- src/features/LiveSyncCommands.ts | 11 +- .../ModuleObsidianSettingAsMarkdown.ts | 1 + .../ObsidianLiveSyncSettingTab.ts | 11 + updates.md | 13 + 9 files changed, 350 insertions(+), 257 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index 83884de..0754a4b 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.24.0.dev-rc4", + "version": "0.24.0.dev-rc5", "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", diff --git a/package-lock.json b/package-lock.json index d46f8d3..5cdffb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc4", + "version": "0.24.0.dev-rc5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc4", + "version": "0.24.0.dev-rc5", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", diff --git a/package.json b/package.json index 30b229a..10a4edf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc4", + "version": "0.24.0.dev-rc5", "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", diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index 010570c..62acee4 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -5,7 +5,6 @@ import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, File import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../../lib/src/common/types.ts"; import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../../common/types.ts"; import { createBlob, createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, getDocDataAsArray, isDocContentSame, isLoadedEntry, isObjectDifferent } from "../../lib/src/common/utils.ts"; -import { Logger } from "../../lib/src/common/logger.ts"; import { digestHash } from "../../lib/src/string_and_binary/hash.ts"; import { arrayBufferToBase64, decodeBinary, readString } from '../../lib/src/string_and_binary/convert.ts'; import { serialized, shareRunningResult } from "../../lib/src/concurrency/lock.ts"; @@ -419,12 +418,12 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { async $everyOnDatabaseInitialized(showNotice: boolean) { if (!this._isThisModuleEnabled()) return true; try { - Logger("Scanning customizations..."); + this._log("Scanning customizations..."); await this.scanAllConfigFiles(showNotice); - Logger("Scanning customizations : done"); + this._log("Scanning customizations : done"); } catch (ex) { - Logger("Scanning customizations : failed"); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log("Scanning customizations : failed"); + this._log(ex, LOG_LEVEL_VERBOSE); } return true; } @@ -477,7 +476,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { xFiles.push(work); } if (missingHash) { - Logger(`Digest created for ${path} to improve checking`, LOG_LEVEL_VERBOSE); + this._log(`Digest created for ${path} to improve checking`, LOG_LEVEL_VERBOSE); wx.data = serialize(data); fireAndForget(() => this.localDatabase.putDBEntry(createSavingEntryFromLoadedEntry(wx))); } @@ -512,8 +511,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return []; } catch (ex) { - Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); } return []; }, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline(); @@ -536,8 +535,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return []; } catch (ex) { - Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); } return []; }, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline(); @@ -583,11 +582,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { if (!loaded) { const d = await this.localDatabase.getDBEntry(unifiedPathV2); if (!d) { - Logger(`The file ${unifiedPathV2} is not found`, LOG_LEVEL_VERBOSE); + this._log(`The file ${unifiedPathV2} is not found`, LOG_LEVEL_VERBOSE); return false; } if (!isLoadedEntry(d)) { - Logger(`The file ${unifiedPathV2} is not a note`, LOG_LEVEL_VERBOSE); + this._log(`The file ${unifiedPathV2} is not a note`, LOG_LEVEL_VERBOSE); return false; } loaded = d; @@ -613,8 +612,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { this.pluginList.filter(e => e instanceof PluginDataExDisplayV2 && e.confKey == confKey).forEach(e => (e as PluginDataExDisplayV2).applyLoadedManifest()); pluginList.set(this.pluginList); } catch (ex) { - Logger(`The file ${loaded.path} seems to manifest, but could not be decoded as JSON`, LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`The file ${loaded.path} seems to manifest, but could not be decoded as JSON`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); } this.loadedManifest_mTime.set(confKey, file.mtime); } else { @@ -687,22 +686,22 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { async migrateV1ToV2(showMessage: boolean, entry: AnyEntry): Promise { const v1Path = entry.path; - Logger(`Migrating ${entry.path} to V2`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); + this._log(`Migrating ${entry.path} to V2`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); if (entry.deleted) { - Logger(`The entry ${v1Path} is already deleted`, LOG_LEVEL_VERBOSE); + this._log(`The entry ${v1Path} is already deleted`, LOG_LEVEL_VERBOSE); return; } if (!v1Path.endsWith(".md") && !v1Path.startsWith(ICXHeader)) { - Logger(`The entry ${v1Path} is not a customisation sync binder`, LOG_LEVEL_VERBOSE); + this._log(`The entry ${v1Path} is not a customisation sync binder`, LOG_LEVEL_VERBOSE); return } if (v1Path.indexOf("%") !== -1) { - Logger(`The entry ${v1Path} is already migrated`, LOG_LEVEL_VERBOSE); + this._log(`The entry ${v1Path} is already migrated`, LOG_LEVEL_VERBOSE); return; } const loadedEntry = await this.localDatabase.getDBEntry(v1Path); if (!loadedEntry) { - Logger(`The entry ${v1Path} is not found`, LOG_LEVEL_VERBOSE); + this._log(`The entry ${v1Path} is not found`, LOG_LEVEL_VERBOSE); return; } @@ -723,7 +722,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const relativeFilename = f.filename.split("/").slice(deletePrefixCount).join("/"); const v2Path = (prefixPath + relativeFilename) as FilePathWithPrefix; // console.warn(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`); - Logger(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE); + this._log(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE); const newId = await this.plugin.$$path2id(v2Path); // const buf = @@ -742,12 +741,12 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } const r = await this.plugin.localDatabase.putDBEntry(saving); if (r && r.ok) { - Logger(`Migrated ${v1Path} / ${f.filename} to ${v2Path}`, LOG_LEVEL_INFO); + this._log(`Migrated ${v1Path} / ${f.filename} to ${v2Path}`, LOG_LEVEL_INFO); const delR = await this.deleteConfigOnDatabase(v1Path); if (delR) { - Logger(`Deleted ${v1Path} successfully`, LOG_LEVEL_INFO); + this._log(`Deleted ${v1Path} successfully`, LOG_LEVEL_INFO); } else { - Logger(`Failed to delete ${v1Path}`, LOG_LEVEL_NOTICE); + this._log(`Failed to delete ${v1Path}`, LOG_LEVEL_NOTICE); } } } @@ -802,9 +801,9 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } const fileA = await loadFile(dataA); const fileB = await loadFile(dataB); - Logger(`Comparing: ${dataA.documentPath} <-> ${dataB.documentPath}`, LOG_LEVEL_VERBOSE); + this._log(`Comparing: ${dataA.documentPath} <-> ${dataB.documentPath}`, LOG_LEVEL_VERBOSE); if (!fileA || !fileB) { - Logger(`Could not load ${dataA.name} for comparison: ${!fileA ? dataA.term : ""}${!fileB ? dataB.term : ""}`, LOG_LEVEL_NOTICE); + this._log(`Could not load ${dataA.name} for comparison: ${!fileA ? dataA.term : ""}${!fileB ? dataB.term : ""}`, LOG_LEVEL_NOTICE); return false; } let path = stripAllPrefixes(fileA.path.split("/").slice(-1).join("/") as FilePath); // TODO:adjust @@ -813,15 +812,15 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } if (fileA.path.endsWith(".json")) { return serialized("config:merge-data", () => new Promise((res) => { - Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE); + this._log("Opening data-merging dialog", LOG_LEVEL_VERBOSE); // const docs = [docA, docB]; const modal = new JsonResolveModal(this.app, path, [fileA, fileB], async (keep, result) => { if (result == null) return res(false); try { res(await this.applyData(dataA, result)); } catch (ex) { - Logger("Could not apply merged file"); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log("Could not apply merged file"); + this._log(ex, LOG_LEVEL_VERBOSE); res(false); } }, "Local", `${dataB.term}`, "B", true, true, "Difference between local and remote"); @@ -847,7 +846,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { right: { rev: "B", ...fileB, data: docBData }, diff: diff } - console.dir(diffResult); + // console.dir(diffResult); const d = new ConflictResolveModal(this.app, path, diffResult, true, dataB.term); d.open(); const ret = await d.waitForResult(); @@ -866,7 +865,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { if (content) { // const dt = createBlob(content); const filename = data.files[0].filename; - Logger(`Applying ${filename} of ${data.displayName || data.name}..`); + this._log(`Applying ${filename} of ${data.displayName || data.name}..`); const path = `${baseDir}/${filename}` as FilePath; await this.plugin.storageAccess.ensureDir(path); // If the content has applied, modified time will be updated to the current time. @@ -879,7 +878,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { // If files have applied, modified time will be updated to the current time. const stat = { mtime: f.mtime, ctime: f.ctime }; const path = `${baseDir}/${f.filename}` as FilePath; - Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`); + this._log(`Applying ${f.filename} of ${data.displayName || data.name}..`); // const contentEach = createBlob(f.data); await this.plugin.storageAccess.ensureDir(path); @@ -888,13 +887,13 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { try { oldData = await this.plugin.storageAccess.readHiddenFileBinary(path); } catch (ex) { - Logger(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); oldData = new ArrayBuffer(0); } const content = base64ToArrayBuffer(f.data); if (await isDocContentSame(oldData, content)) { - Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); + this._log(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); continue; } await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat); @@ -903,30 +902,30 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { try { oldData = await this.plugin.storageAccess.readHiddenFileText(path); } catch (ex) { - Logger(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); oldData = ""; } const content = getDocData(f.data); if (await isDocContentSame(oldData, content)) { - Logger(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); + this._log(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE); continue; } await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat); } - Logger(`Applied ${f.filename} of ${data.displayName || data.name}..`); + this._log(`Applied ${f.filename} of ${data.displayName || data.name}..`); await this.storeCustomisationFileV2(path, this.plugin.deviceAndVaultName); } } } catch (ex) { - Logger(`Applying ${data.displayName || data.name}.. Failed`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Applying ${data.displayName || data.name}.. Failed`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } return true; } async applyData(data: IPluginDataExDisplay, content?: string): Promise { - Logger(`Applying ${data.displayName || data.name + this._log(`Applying ${data.displayName || data.name }..`); if (data instanceof PluginDataExDisplayV2) { @@ -941,7 +940,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } const loadedData = deserialize(getDocDataAsArray(dx.data), {}) as PluginDataEx; for (const f of loadedData.files) { - Logger(`Applying ${f.filename} of ${data.displayName || data.name}..`); + this._log(`Applying ${f.filename} of ${data.displayName || data.name}..`); try { // console.dir(f); const path = `${baseDir}/${f.filename}`; @@ -952,11 +951,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } else { await this.plugin.storageAccess.writeHiddenFileAuto(path, content); } - Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`); + this._log(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`); } catch (ex) { - Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Applying ${f.filename} of ${data.displayName || data.name}.. Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); } } @@ -964,7 +963,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { await this.storeCustomizationFiles(uPath); await this.updatePluginList(true, uPath); await delay(100); - Logger(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE); + this._log(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE); if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") { //@ts-ignore const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[]; @@ -972,20 +971,20 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const enabledPlugins = this.app.plugins.enabledPlugins as Set; const pluginManifest = manifests.find((manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}`); if (pluginManifest) { - Logger(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); + this._log(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); // @ts-ignore await this.app.plugins.unloadPlugin(pluginManifest.id); // @ts-ignore await this.app.plugins.loadPlugin(pluginManifest.id); - Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); + this._log(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); } } else if (data.category == "CONFIG") { this.plugin.$$askReload(); } return true; } catch (ex) { - Logger(`Applying ${data.displayName || data.name}.. Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Applying ${data.displayName || data.name}.. Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } } @@ -1007,12 +1006,12 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { await Promise.allSettled(p); // await this.deleteConfigOnDatabase(data.documentPath); // await this.updatePluginList(false, data.documentPath); - Logger(`Deleted: ${data.category}/${data.name} of ${data.category} (${delList.length} items)`, LOG_LEVEL_NOTICE); + this._log(`Deleted: ${data.category}/${data.name} of ${data.category} (${delList.length} items)`, LOG_LEVEL_NOTICE); } return true; } catch (ex) { - Logger(`Failed to delete: ${data.documentPath}`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Failed to delete: ${data.documentPath}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } @@ -1096,13 +1095,13 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { displayName = `${json.name}`; } } catch (ex) { - Logger(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL_INFO); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL_INFO); + this._log(ex, LOG_LEVEL_VERBOSE); } } } catch (ex) { - Logger(`The file ${path} could not be encoded`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`The file ${path} could not be encoded`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } const mtime = stat.mtime; @@ -1150,7 +1149,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { }; } else { if (isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, LOG_LEVEL_DEBUG); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, LOG_LEVEL_DEBUG); return; } const docXDoc = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false); @@ -1162,7 +1161,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const oldContent = dataSrc.substring(dataStart + DUMMY_END.length); const oldContentArray = base64ToArrayBuffer(oldContent); if (await isDocContentSame(oldContentArray, content)) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, LOG_LEVEL_VERBOSE); markChangesAreSame(prefixedFileName, old.mtime, mtime + 1); return true; } @@ -1179,12 +1178,12 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { }; } const ret = await this.localDatabase.putDBEntry(saveData); - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Done`); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Done`); fireAndForget(() => this.updatePluginListV2(false, this.filenameWithUnifiedKey(path))); return ret; } catch (ex) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } }) @@ -1192,7 +1191,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { async storeCustomizationFiles(path: FilePath, termOverRide?: string) { const term = termOverRide || this.plugin.deviceAndVaultName; if (term == "") { - Logger("We have to configure the device name", LOG_LEVEL_NOTICE); + this._log("We have to configure the device name", LOG_LEVEL_NOTICE); return; } if (this.useV2) { @@ -1234,7 +1233,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { for (const target of fileTargets) { const data = await this.makeEntryFromFile(target); if (data == false) { - Logger(`Config: skipped (Possibly is not exist): ${target} `, LOG_LEVEL_VERBOSE); + this._log(`Config: skipped (Possibly is not exist): ${target} `, LOG_LEVEL_VERBOSE); continue; } if (data.version) { @@ -1249,9 +1248,9 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } dt.mtime = mtime; - // Logger(`Configuration saving: ${prefixedFileName}`); + // this._log(`Configuration saving: ${prefixedFileName}`); if (dt.files.length == 0) { - Logger(`Nothing left: deleting.. ${path}`); + this._log(`Nothing left: deleting.. ${path}`); await this.deleteConfigOnDatabase(prefixedFileName); await this.updatePluginList(false, prefixedFileName); return @@ -1277,7 +1276,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { }; } else { if (old.mtime == mtime) { - // Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same time)`, LOG_LEVEL_VERBOSE); + // this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same time)`, LOG_LEVEL_VERBOSE); return true; } const oldC = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false); @@ -1289,7 +1288,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { })) const isSame = (await Promise.all(diffs)).every(e => e == true); if (isSame) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same content)`, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same content)`, LOG_LEVEL_VERBOSE); return true; } } @@ -1308,25 +1307,24 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } const ret = await this.localDatabase.putDBEntry(saveData); await this.updatePluginList(false, saveData.path); - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Done`); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Done`); return ret; } catch (ex) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${prefixedFileName}: (config) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } }) - } async $anyProcessOptionalFileEvent(path: FilePath): Promise { return await this.watchVaultRawEventsAsync(path); } async watchVaultRawEventsAsync(path: FilePath) { - if (!this._isMainReady) return true; - if (!this._isMainSuspended()) return true; - if (!this._isThisModuleEnabled()) return true; - if (!this.isTargetPath(path)) return false; + if (!this._isMainReady) return false; + if (this._isMainSuspended()) return false; + if (!this._isThisModuleEnabled()) return false; + // if (!this.isTargetPath(path)) return false; const stat = await this.plugin.storageAccess.statHidden(path); // Make sure that target is a file. if (stat && stat.type != "file") @@ -1337,10 +1335,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { e.mode != MODE_SELECTIVE && e.mode != MODE_SHINY ).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) { - Logger(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE); + this._log(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE); // This file could be handled by the other module. return false; } + // this._log(`Customization file detected: ${path}`, LOG_LEVEL_VERBOSE); const storageMTime = ~~((stat && stat.mtime || 0) / 1000); const key = `${path}-${storageMTime}`; if (this.recentProcessedInternalFiles.contains(key)) { @@ -1359,16 +1358,13 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return true; } - - - async scanAllConfigFiles(showMessage: boolean) { await shareRunningResult("scanAllConfigFiles", async () => { const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; - Logger("Scanning customizing files.", logLevel, "scan-all-config"); + this._log("Scanning customizing files.", logLevel, "scan-all-config"); const term = this.plugin.deviceAndVaultName; if (term == "") { - Logger("We have to configure the device name", LOG_LEVEL_NOTICE); + this._log("We have to configure the device name", LOG_LEVEL_NOTICE); return; } const filesAll = await this.scanInternalFiles(); @@ -1396,8 +1392,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { await this.deleteConfigOnDatabase(unifiedFilenameWithKey); } } catch (ex) { - Logger(`scanAllConfigFiles - Error: ${item._id}`, LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`scanAllConfigFiles - Error: ${item._id}`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); } finally { releaser(); } @@ -1412,8 +1408,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { try { await this.storeCustomisationFileV2(filePath, term); } catch (ex) { - Logger(`scanAllConfigFiles - Error: ${filePath}`, LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`scanAllConfigFiles - Error: ${filePath}`, LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); } finally { releaser(); @@ -1430,7 +1426,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { for (const vp of virtualPathsOfLocalFiles) { const p = files.find(e => e.key == vp)?.file; if (!p) { - Logger(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE); + this._log(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE); continue; } await this.storeCustomizationFiles(p); @@ -1453,11 +1449,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, false) as InternalFileEntry | false; let saveData: InternalFileEntry; if (old === false) { - Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted (Not found on database)`); + this._log(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted (Not found on database)`); return true; } else { if (old.deleted) { - Logger(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted`); + this._log(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted`); return true; } saveData = @@ -1472,11 +1468,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } await this.localDatabase.putRaw(saveData); await this.updatePluginList(false, prefixedFileName); - Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Done`); + this._log(`STORAGE -x> DB:${prefixedFileName}: (config) Done`); return true; } catch (ex) { - Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`STORAGE -x> DB:${prefixedFileName}: (config) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } }); @@ -1530,7 +1526,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { $allSuspendExtraSync(): Promise { if (this.plugin.settings.usePluginSync || this.plugin.settings.autoSweepPlugins) { - Logger("Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) + this._log("Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) this.plugin.settings.usePluginSync = false; this.plugin.settings.autoSweepPlugins = false; } @@ -1590,8 +1586,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { try { w = await this.app.vault.adapter.list(path); } catch (ex) { - Logger(`Could not traverse(ConfigSync):${path}`, LOG_LEVEL_INFO); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Could not traverse(ConfigSync):${path}`, LOG_LEVEL_INFO); + this._log(ex, LOG_LEVEL_VERBOSE); return []; } let files = [ diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index e7ef060..6e24a43 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -1,9 +1,8 @@ import { normalizePath, type PluginManifest, type ListedFiles } from "../../deps.ts"; -import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry, type DocumentID } from "../../lib/src/common/types.ts"; +import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry, type DocumentID, type UXStat, MODE_AUTOMATIC } from "../../lib/src/common/types.ts"; import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "../../common/types.ts"; import { readAsBlob, isDocContentSame, sendSignal, readContent, createBlob, fireAndForget } from "../../lib/src/common/utils.ts"; -import { Logger } from "../../lib/src/common/logger.ts"; -import { getPath, isInternalMetadata, PeriodicProcessor } from "../../common/utils.ts"; +import { BASE_IS_NEW, compareMTime, EVEN, getPath, isInternalMetadata, isMarkedAsSameChanges, markChangesAreSame, PeriodicProcessor, TARGET_IS_NEW } from "../../common/utils.ts"; import { serialized } from "../../lib/src/concurrency/lock.ts"; import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts"; import { LiveSyncCommands } from "../LiveSyncCommands.ts"; @@ -41,12 +40,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule async $everyOnDatabaseInitialized(showNotice: boolean) { if (this._isThisModuleEnabled()) { try { - Logger("Synchronizing hidden files..."); + this._log("Synchronizing hidden files..."); await this.syncInternalFilesAndDatabase("push", showNotice); - Logger("Synchronizing hidden files done"); + this._log("Synchronizing hidden files done"); } catch (ex) { - Logger("Synchronizing hidden files failed"); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log("Synchronizing hidden files failed"); + this._log(ex, LOG_LEVEL_VERBOSE); } } return true; @@ -58,6 +57,13 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule return true; } + $everyOnloadAfterLoadSettings(): Promise { + const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns + .replace(/\n| /g, "") + .split(",").filter(e => e).map(e => new RegExp(e, "i")); + this.ignorePatterns = ignorePatterns; + return Promise.resolve(true); + } async $everyOnResumeProcess(): Promise { this.periodicInternalFileScanProcessor?.disable(); @@ -77,6 +83,10 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule if (!this.plugin.isReady) return Promise.resolve(true); this.periodicInternalFileScanProcessor.enable(this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); + const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns + .replace(/\n| /g, "") + .split(",").filter(e => e).map(e => new RegExp(e, "i")); + this.ignorePatterns = ignorePatterns; return Promise.resolve(true); } @@ -85,56 +95,56 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } internalFileProcessor = new QueueProcessor( async (filenames) => { - Logger(`START :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE); + this._log(`START :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE); await this.syncInternalFilesAndDatabase("pull", false, false, filenames); - Logger(`DONE :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE); + this._log(`DONE :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE); return; }, { batchSize: 100, concurrentLimit: 1, delay: 10, yieldThreshold: 100, suspended: false, totalRemainingReactiveSource: hiddenFilesEventCount } ); - recentProcessedInternalFiles = [] as string[]; - async $anyProcessOptionalFileEvent(path: FilePath): Promise { return await this.watchVaultRawEventsAsync(path); } async watchVaultRawEventsAsync(path: FilePath): Promise { + if (!this._isMainReady) return false; + if (this._isMainSuspended()) return false; if (!this._isThisModuleEnabled()) return false; - if (!isInternalMetadata(path)) return false; // Exclude files handled by customization sync const configDir = normalizePath(this.app.vault.configDir); - const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); + const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : + Object.values(this.settings.pluginSyncExtendedSetting). + filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED). + map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) { - Logger(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE); + this._log(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE); return false; } + const stat = await this.plugin.storageAccess.statHidden(path); // sometimes folder is coming. if (stat != null && stat.type != "file") { return false; } - const mtime = stat == null ? 0 : stat?.mtime ?? 0; - const storageMTime = ~~((mtime) / 1000); - const key = `${path}-${storageMTime}`; - if (mtime != 0 && this.recentProcessedInternalFiles.contains(key)) { - //If recently processed, it may caused by self. - // Return true to prevent further processing. + + if (this.isKnownChange(path, stat?.mtime ?? 0)) { + // This could be caused by self. so return true to prevent further processing. return true; } - this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100); - // const id = await this.path2id(path, ICHeader); + const mtime = stat == null ? 0 : stat?.mtime ?? 0; + const storageMTime = ~~((mtime) / 1000); + const prefixedFileName = addPrefix(path, ICHeader); const filesOnDB = await this.localDatabase.getDBEntryMeta(prefixedFileName); const dbMTime = ~~((filesOnDB && filesOnDB.mtime || 0) / 1000); // Skip unchanged file. if (dbMTime == storageMTime) { - // Logger(`STORAGE --> DB:${path}: (hidden) Nothing changed`); + // this._log(`STORAGE --> DB:${path}: (hidden) Nothing changed`); // Handled, but nothing changed. also return true to prevent further processing. return true; } - // Do not compare timestamp. Always local data should be preferred except this plugin wrote one. try { if (storageMTime == 0) { await this.deleteInternalFileOnDatabase(path); @@ -144,8 +154,8 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule // Surely processed. return true; } catch (ex) { - Logger(`Failed to process hidden file:${path}`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Failed to process hidden file:${path}`); + this._log(ex, LOG_LEVEL_VERBOSE); } // Could not be processed. but it was own task. so return true to prevent further processing. return true; @@ -164,8 +174,8 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } } } catch (ex) { - Logger("something went wrong on resolving all conflicted internal files"); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log("something went wrong on resolving all conflicted internal files"); + this._log(ex, LOG_LEVEL_VERBOSE); } await this.conflictResolutionProcessor.startPipeline().waitForAllProcessed(); } @@ -176,12 +186,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule // simply check modified time const mtimeCurrent = ("mtime" in currentDoc && currentDoc.mtime) || 0; const mtimeConflicted = ("mtime" in conflictedDoc && conflictedDoc.mtime) || 0; - // Logger(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`); + // this._log(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`); // console.log(`mtime:${mtimeA} - ${mtimeB}`); const delRev = mtimeCurrent < mtimeConflicted ? currentRev : conflictedRev; // delete older one. await this.localDatabase.removeRevision(id, delRev); - Logger(`Older one has been deleted:${path}`); + this._log(`Older one has been deleted:${path}`); const cc = await this.localDatabase.getRaw(id, { conflicts: true }); if (cc._conflicts?.length === 0) { await this.extractInternalFileFromDatabase(stripAllPrefixes(path)) @@ -204,7 +214,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule if (doc._conflicts === undefined) return []; if (doc._conflicts.length == 0) return []; - Logger(`Hidden file conflicted:${path}`); + this._log(`Hidden file conflicted:${path}`); const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0])); const revA = doc._rev; const revB = conflicts[0]; @@ -217,7 +227,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule const commonBase = revFrom._revs_info?.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? ""; const result = await this.plugin.localDatabase.mergeObject(path, commonBase, doc._rev, conflictedRev); if (result) { - Logger(`Object merge:${path}`, LOG_LEVEL_INFO); + this._log(`Object merge:${path}`, LOG_LEVEL_INFO); const filename = stripAllPrefixes(path); const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(filename); if (!isExists) { @@ -234,7 +244,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule this.conflictResolutionProcessor.enqueue(path); return []; } else { - Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE); + this._log(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE); } return [{ path, revA, revB, id, doc }]; } @@ -242,8 +252,8 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule await this.resolveByNewerEntry(id, path, doc, revA, revB); return []; } catch (ex) { - Logger(`Failed to resolve conflict (Hidden): ${path}`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Failed to resolve conflict (Hidden): ${path}`); + this._log(ex, LOG_LEVEL_VERBOSE); return []; } }, { @@ -281,7 +291,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule this.procInternalFile(filename); return true; } else { - Logger(`Skipped (Not target:${filename})`, LOG_LEVEL_VERBOSE); + this._log(`Skipped (Not target:${filename})`, LOG_LEVEL_VERBOSE); return false; } } @@ -291,30 +301,46 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule this.conflictResolutionProcessor.enqueue(path); } + knownChanges: { [key: string]: number; } = {}; + markAsKnownChange(path: string, mtime: number) { + this.knownChanges[path] = mtime; + } + isKnownChange(path: string, mtime: number) { + return this.knownChanges[path] == mtime; + } + ignorePatterns: RegExp[] = []; //TODO: Tidy up. Even though it is experimental feature, So dirty... - async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, filesAll: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) { + async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, filesAll: InternalFileInfo[] | false = false, targetFilesSrc: string[] | false = false) { + const targetFiles = targetFilesSrc ? targetFilesSrc.map(e => stripAllPrefixes(e as FilePathWithPrefix)) : false; + // debugger; await this.resolveConflictOnInternalFiles(); const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; - Logger("Scanning hidden files.", logLevel, "sync_internal"); - const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns - .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); + this._log("Scanning hidden files.", logLevel, "sync_internal"); const configDir = normalizePath(this.app.vault.configDir); let files: InternalFileInfo[] = filesAll ? filesAll : (await this.scanInternalFiles()) + const allowedInHiddenFileSync = this.settings.usePluginSync ? Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_AUTOMATIC).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()) : undefined; + if (allowedInHiddenFileSync) { + const systemOrNot = files.reduce((acc, cur) => { + if (cur.path.startsWith(configDir)) { + acc.system.push(cur); + } else { + acc.user.push(cur); + } + return acc; + }, { system: [] as InternalFileInfo[], user: [] as InternalFileInfo[] }); - const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); - files = files.filter(file => synchronisedInConfigSync.every(filterFile => !file.path.toLowerCase().startsWith(filterFile))) + files = + [...systemOrNot.user, + ...systemOrNot.system.filter(file => allowedInHiddenFileSync.some(filterFile => file.path.toLowerCase().startsWith(filterFile)))]; + } const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted); const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => stripAllPrefixes(this.getPath(e)))])]; - const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)).filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))) - function compareMTime(a: number, b: number) { - const wa = ~~(a / 1000); - const wb = ~~(b / 1000); - const diff = wa - wb; - return diff; + let allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)); + if (allowedInHiddenFileSync) { + allFileNames = allFileNames.filter(file => allowedInHiddenFileSync.some(filterFile => file.toLowerCase().startsWith(filterFile))); } const fileCount = allFileNames.length; @@ -342,9 +368,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule c = pieces.shift(); } }; - // Cache update time information for files which have already been processed (mainly for files that were skipped due to the same content) - let caches: { [key: string]: { storageMtime: number; docMtime: number; }; } = {}; - caches = await this.kvDB.get<{ [key: string]: { storageMtime: number; docMtime: number; }; }>("diff-caches-internal") || {}; + const filesMap = files.reduce((acc, cur) => { acc[cur.path] = cur; return acc; @@ -357,10 +381,10 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule const filename = filenames[0]; processed++; if (processed % 100 == 0) { - Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal"); + this._log(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal"); } if (!filename) return []; - if (ignorePatterns.some(e => filename.match(e))) + if (this.ignorePatterns.some(e => filename.match(e))) return []; if (await this.plugin.$$isIgnoredByIgnoreFiles(filename)) { return []; @@ -384,25 +408,24 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule fileOnDatabase: xFileOnDatabase } = params[0]; if (xFileOnStorage && xFileOnDatabase) { - const cache = filename in caches ? caches[filename] : { storageMtime: 0, docMtime: 0 }; // Both => Synchronize - if ((direction != "pullForce" && direction != "pushForce") && xFileOnDatabase.mtime == cache.docMtime && xFileOnStorage.mtime == cache.storageMtime) { + if ((direction != "pullForce" && direction != "pushForce") && isMarkedAsSameChanges(filename, [xFileOnDatabase.mtime, xFileOnStorage.mtime]) == EVEN) { + this._log(`Hidden file skipped: ${filename} is marked as same`, LOG_LEVEL_VERBOSE); return; } + const nw = compareMTime(xFileOnStorage.mtime, xFileOnDatabase.mtime); - if (nw > 0 || direction == "pushForce") { - await this.storeInternalFileToDatabase(xFileOnStorage); - } - if (nw < 0 || direction == "pullForce") { + if (nw == BASE_IS_NEW || direction == "pushForce") { + if (await this.storeInternalFileToDatabase(xFileOnStorage) !== false) { + // countUpdatedFolder(filename); + } + } else if (nw == TARGET_IS_NEW || direction == "pullForce") { // skip if not extraction performed. - if (!await this.extractInternalFileFromDatabase(filename)) - return; + if (await this.extractInternalFileFromDatabase(filename)) + countUpdatedFolder(filename); + } else { + // Even, or not forced. skip. } - // If process successfully updated or file contents are same, update cache. - cache.docMtime = xFileOnDatabase.mtime; - cache.storageMtime = xFileOnStorage.mtime; - caches[filename] = cache; - countUpdatedFolder(filename); } else if (!xFileOnStorage && xFileOnDatabase) { if (direction == "push" || direction == "pushForce") { if (xFileOnDatabase.deleted) @@ -423,7 +446,9 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule if (direction == "push" || direction == "pushForce" || direction == "safe") { await this.storeInternalFileToDatabase(xFileOnStorage); } else { - await this.extractInternalFileFromDatabase(xFileOnStorage.path); + // if (await this.extractInternalFileFromDatabase(xFileOnStorage.path)) { + // countUpdatedFolder(xFileOnStorage.path); + // } } } else { throw new Error("Invalid state on hidden file sync"); @@ -435,8 +460,6 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule .enqueueAll(allFileNames) .startPipeline().waitForAllDoneAndTerminate(); - await this.kvDB.set("diff-caches-internal", caches); - // When files has been retrieved from the database. they must be reloaded. if ((direction == "pull" || direction == "pullForce") && filesChanged != 0) { // Show notification to restart obsidian when something has been changed in configDir. @@ -459,12 +482,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule anchor.text = "HERE"; anchor.addEventListener("click", () => { fireAndForget(async () => { - Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); + this._log(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); // @ts-ignore await this.app.plugins.unloadPlugin(updatePluginId); // @ts-ignore await this.app.plugins.loadPlugin(updatePluginId); - Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); + this._log(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); }); }); } @@ -472,8 +495,8 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } } } catch (ex) { - Logger("Error on checking plugin status."); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log("Error on checking plugin status."); + this._log(ex, LOG_LEVEL_VERBOSE); } @@ -491,17 +514,19 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } } - Logger(`Hidden files scanned: ${filesChanged} files had been modified`, logLevel, "sync_internal"); + this._log(`Hidden files scanned: ${filesChanged} files had been modified`, logLevel, "sync_internal"); } async storeInternalFileToDatabase(file: InternalFileInfo, forceWrite = false) { - if (await this.plugin.$$isIgnoredByIgnoreFiles(file.path)) { - return + const storeFilePath = file.path; + const storageFilePath = file.path; + if (await this.plugin.$$isIgnoredByIgnoreFiles(storageFilePath)) { + return undefined; } - const id = await this.path2id(file.path, ICHeader); - const prefixedFileName = addPrefix(file.path, ICHeader); - const content = createBlob(await this.plugin.storageAccess.readHiddenFileAuto(file.path)); + const id = await this.path2id(storeFilePath, ICHeader); + const prefixedFileName = addPrefix(storeFilePath, ICHeader); + const content = createBlob(await this.plugin.storageAccess.readHiddenFileAuto(storageFilePath)); const mtime = file.mtime; return await serialized("file-" + prefixedFileName, async () => { try { @@ -523,8 +548,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule }; } else { if (await isDocContentSame(readAsBlob(old), content) && !forceWrite) { - // Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE); - return; + // this._log(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE); + const stat = await this.plugin.storageAccess.statHidden(storageFilePath); + if (stat) { + markChangesAreSame(storageFilePath, old.mtime, stat.mtime); + } + return undefined; } saveData = { @@ -539,24 +568,32 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule }; } const ret = await this.localDatabase.putDBEntry(saveData); - Logger(`STORAGE --> DB:${file.path}: (hidden) Done`); - return ret; + if (ret !== false) { + this._log(`STORAGE --> DB:${storageFilePath}: (hidden) Done`); + return true; + } else { + this._log(`STORAGE --> DB:${storageFilePath}: (hidden) Failed`); + return false; + } } catch (ex) { - Logger(`STORAGE --> DB:${file.path}: (hidden) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`STORAGE --> DB:${storageFilePath}: (hidden) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } }); } - async deleteInternalFileOnDatabase(filename: FilePath, forceWrite = false) { - const id = await this.path2id(filename, ICHeader); - const prefixedFileName = addPrefix(filename, ICHeader); + async deleteInternalFileOnDatabase(filenameSrc: FilePath, forceWrite = false) { + const storeFilePath = filenameSrc; + const storageFilePath = filenameSrc; + const displayFileName = filenameSrc; + const id = await this.path2id(storeFilePath, ICHeader); + const prefixedFileName = addPrefix(storeFilePath, ICHeader); const mtime = new Date().getTime(); - if (await this.plugin.$$isIgnoredByIgnoreFiles(filename)) { - return + if (await this.plugin.$$isIgnoredByIgnoreFiles(storageFilePath)) { + return undefined } - await serialized("file-" + prefixedFileName, async () => { + return await serialized("file-" + prefixedFileName, async () => { try { const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, true) as InternalFileEntry | false; let saveData: InternalFileEntry; @@ -578,12 +615,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule if (conflicts._conflicts !== undefined) { for (const conflictRev of conflicts._conflicts) { await this.localDatabase.removeRevision(old._id, conflictRev); - Logger(`STORAGE -x> DB:${filename}: (hidden) conflict removed ${old._rev} => ${conflictRev}`, LOG_LEVEL_VERBOSE); + this._log(`STORAGE -x> DB: ${displayFileName}: (hidden) conflict removed ${old._rev} => ${conflictRev}`, LOG_LEVEL_VERBOSE); } } if (old.deleted) { - Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`); - return; + this._log(`STORAGE -x> DB: ${displayFileName}: (hidden) already deleted`); + return undefined; } saveData = { @@ -595,85 +632,104 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule type: "newnote", }; } - await this.localDatabase.putRaw(saveData); - Logger(`STORAGE -x> DB:${filename}: (hidden) Done`); + const ret = await this.localDatabase.putRaw(saveData); + if (ret && ret.ok) { + this._log(`STORAGE -x> DB: ${displayFileName}: (hidden) Done`); + return true; + } else { + this._log(`STORAGE -x> DB: ${displayFileName}: (hidden) Failed`); + return false; + } } catch (ex) { - Logger(`STORAGE -x> DB:${filename}: (hidden) Failed`); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`STORAGE -x> DB: ${displayFileName}: (hidden) Failed`); + this._log(ex, LOG_LEVEL_VERBOSE); return false; } }); } - async extractInternalFileFromDatabase(filename: FilePath, force = false) { - const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(filename); - const prefixedFileName = addPrefix(filename, ICHeader); - if (await this.plugin.$$isIgnoredByIgnoreFiles(filename)) { - return; + async extractInternalFileFromDatabase(filenameSrc: FilePath, force = false) { + const storeFilePath = filenameSrc; + const storageFilePath = filenameSrc; + const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(storageFilePath); + const prefixedFileName = addPrefix(storeFilePath, ICHeader); + const displayFileName = `${storeFilePath}`; + if (await this.plugin.$$isIgnoredByIgnoreFiles(storageFilePath)) { + return undefined; } return await serialized("file-" + prefixedFileName, async () => { try { // Check conflicted status const fileOnDB = await this.localDatabase.getDBEntry(prefixedFileName, { conflicts: true }, false, true, true); if (fileOnDB === false) - throw new Error(`File not found on database.:${filename}`); + throw new Error(`File not found on database.:${displayFileName}`); // Prevent overwrite for Prevent overwriting while some conflicted revision exists. if (fileOnDB?._conflicts?.length) { - Logger(`Hidden file ${filename} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO); - return; + this._log(`Hidden file ${displayFileName} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO); + return false; } const deleted = fileOnDB.deleted || fileOnDB._deleted || false; if (deleted) { if (!isExists) { - Logger(`STORAGE { return new Promise((res) => { - Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE); + this._log("Opening data-merging dialog", LOG_LEVEL_VERBOSE); const docs = [docA, docB]; - const path = stripAllPrefixes(docA.path); - const modal = new JsonResolveModal(this.app, path, [docA, docB], async (keep, result) => { + const strippedPath = stripAllPrefixes(docA.path); + const storageFilePath = strippedPath; + const storeFilePath = strippedPath; + const displayFilename = `${storeFilePath}`; + // const path = this.prefixedConfigDir2configDir(stripAllPrefixes(docA.path)) || docA.path; + const modal = new JsonResolveModal(this.app, storageFilePath, [docA, docB], async (keep, result) => { // modal.close(); try { - const filename = path; + // const filename = storeFilePath; let needFlush = false; if (!result && !keep) { - Logger(`Skipped merging: ${filename}`); + this._log(`Skipped merging: ${displayFilename}`); res(false); return; } @@ -701,41 +761,44 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule for (const doc of docs) { if (doc._rev != keep) { if (await this.localDatabase.deleteDBEntry(this.getPath(doc), { rev: doc._rev })) { - Logger(`Conflicted revision has been deleted: ${filename}`); + this._log(`Conflicted revision has been deleted: ${displayFilename}`); needFlush = true; } } } } if (!keep && result) { - const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(filename); + const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(storageFilePath); if (!isExists) { - await this.plugin.storageAccess.ensureDir(filename); + await this.plugin.storageAccess.ensureDir(storageFilePath); } - await this.plugin.storageAccess.writeHiddenFileAuto(filename, result); - const stat = await this.plugin.storageAccess.statHidden(filename); + await this.plugin.storageAccess.writeHiddenFileAuto(storageFilePath, result); + const stat = await this.plugin.storageAccess.statHidden(storageFilePath); if (!stat) { throw new Error("Stat failed"); } const mtime = stat?.mtime ?? 0; - await this.storeInternalFileToDatabase({ path: filename, mtime, ctime: stat?.ctime ?? mtime, size: stat?.size ?? 0 }, true); + await this.storeInternalFileToDatabase({ path: storageFilePath, mtime, ctime: stat?.ctime ?? mtime, size: stat?.size ?? 0 }, true); try { //@ts-ignore internalAPI - await this.app.vault.adapter.reconcileInternalFile(filename); + await this.app.vault.adapter.reconcileInternalFile(storageFilePath); } catch (ex) { - Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE); + this._log(ex, LOG_LEVEL_VERBOSE); } - Logger(`STORAGE <-- DB:${filename}: written (hidden,merged)`); + this._log(`STORAGE <-- DB:${displayFilename}: written (hidden,merged)`); } if (needFlush) { - await this.extractInternalFileFromDatabase(filename, false); - Logger(`STORAGE --> DB:${filename}: extracted (hidden,merged)`); + if (await this.extractInternalFileFromDatabase(storeFilePath, false)) { + this._log(`STORAGE --> DB:${displayFilename}: extracted (hidden,merged)`); + } else { + this._log(`STORAGE --> DB:${displayFilename}: extracted (hidden,merged) Failed`); + } } res(true); } catch (ex) { - Logger("Could not merge conflicted json"); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log("Could not merge conflicted json"); + this._log(ex, LOG_LEVEL_VERBOSE); res(false); } }); @@ -789,7 +852,7 @@ ${messageFetch}${messageOverwrite}${messageMerge} $allSuspendExtraSync(): Promise { if (this.plugin.settings.syncInternalFiles) { - Logger("Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) + this._log("Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) this.plugin.settings.syncInternalFiles = false; } return Promise.resolve(true); @@ -810,7 +873,7 @@ ${messageFetch}${messageOverwrite}${messageMerge} await this.plugin.saveSettings(); return; } - Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE); + this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE); if (mode == "FETCH") { await this.syncInternalFilesAndDatabase("pullForce", true); } else if (mode == "OVERWRITE") { @@ -821,7 +884,7 @@ ${messageFetch}${messageOverwrite}${messageMerge} this.plugin.settings.useAdvancedMode = true; this.plugin.settings.syncInternalFiles = true; await this.plugin.saveSettings(); - Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE); + this._log(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE); } async scanInternalFiles(): Promise { @@ -869,8 +932,8 @@ ${messageFetch}${messageOverwrite}${messageMerge} try { w = await this.app.vault.adapter.list(path); } catch (ex) { - Logger(`Could not traverse(HiddenSync):${path}`, LOG_LEVEL_INFO); - Logger(ex, LOG_LEVEL_VERBOSE); + this._log(`Could not traverse(HiddenSync):${path}`, LOG_LEVEL_INFO); + this._log(ex, LOG_LEVEL_VERBOSE); return []; } const filesSrc = [ diff --git a/src/features/LiveSyncCommands.ts b/src/features/LiveSyncCommands.ts index d0e3fa3..e21ab57 100644 --- a/src/features/LiveSyncCommands.ts +++ b/src/features/LiveSyncCommands.ts @@ -1,5 +1,6 @@ +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"; @@ -40,4 +41,12 @@ export abstract class LiveSyncCommands { _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); + }; } diff --git a/src/modules/features/ModuleObsidianSettingAsMarkdown.ts b/src/modules/features/ModuleObsidianSettingAsMarkdown.ts index cfb0380..d52dc01 100644 --- a/src/modules/features/ModuleObsidianSettingAsMarkdown.ts +++ b/src/modules/features/ModuleObsidianSettingAsMarkdown.ts @@ -162,6 +162,7 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp const saveData = { ...(settings ? settings : this.settings) } as Partial; delete saveData.encryptedCouchDBConnection; delete saveData.encryptedPassphrase; + delete saveData.additionalSuffixOfDatabaseName; if (!saveData.writeCredentialsForSettingSync && !keepCredential) { delete saveData.couchDB_USER; delete saveData.couchDB_PASSWORD; diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 8995f5d..36d1865 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -1727,6 +1727,17 @@ However, your report is needed to stabilise this. I appreciate you for your grea const enableOnlyOnPluginSyncIsNotEnabled = enableOnly(() => this.isConfiguredAs("usePluginSync", false)); const visibleOnlyOnPluginSyncEnabled = visibleOnly(() => this.isConfiguredAs("usePluginSync", true)); + this.createEl(paneEl, "div", { + text: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.", + cls: "op-warn" + }, c => { + }, visibleOnly(() => this.isConfiguredAs("deviceAndVaultName", ""))); + this.createEl(paneEl, "div", { + text: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.", + cls: "op-warn-info" + }, c => { + }, visibleOnly(() => this.isConfiguredAs("usePluginSync", true))); + new Setting(paneEl) .autoWireText("deviceAndVaultName", { placeHolder: "desktop", onUpdate: enableOnlyOnPluginSyncIsNotEnabled diff --git a/updates.md b/updates.md index 8c7847a..a6003ba 100644 --- a/updates.md +++ b/updates.md @@ -23,6 +23,19 @@ Thank you, and I hope your troubles will be resolved! --- +## 0.24.0.dev-rc5 + +### Improved + +- A note relating to device names has been added to Customisation Sync on the setting dialogue. +- Logs of Hidden File Sync and Customisation Sync have been prefixed with the respective feature names. + +### Fixed + +- Hidden file sync is now working correctly. +- Customisation Sync is now working correctly together with hidden file sync +- No longer database suffix is stored in the setting sharing markdown. + ## 0.24.0.dev-rc4 ### Improved From 5b4ae3703012d726bc50c44bdfb52d882b772128 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Tue, 22 Oct 2024 10:08:20 +0100 Subject: [PATCH 6/8] 0.24.0.dev-rc6 --- manifest-beta.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/features/HiddenFileCommon/JsonResolveModal.ts | 5 ++++- updates.md | 7 +++++++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index 0754a4b..93a389b 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.24.0.dev-rc5", + "version": "0.24.0.dev-rc6", "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", diff --git a/package-lock.json b/package-lock.json index 5cdffb4..262de85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc5", + "version": "0.24.0.dev-rc6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc5", + "version": "0.24.0.dev-rc6", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", diff --git a/package.json b/package.json index 10a4edf..cbf35e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc5", + "version": "0.24.0.dev-rc6", "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", diff --git a/src/features/HiddenFileCommon/JsonResolveModal.ts b/src/features/HiddenFileCommon/JsonResolveModal.ts index 4b9a466..90369a6 100644 --- a/src/features/HiddenFileCommon/JsonResolveModal.ts +++ b/src/features/HiddenFileCommon/JsonResolveModal.ts @@ -32,9 +32,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; } diff --git a/updates.md b/updates.md index a6003ba..56ff6a9 100644 --- a/updates.md +++ b/updates.md @@ -23,6 +23,13 @@ Thank you, and I hope your troubles will be resolved! --- +## 0.24.0.dev-rc6 + +### Fixed + +- We can resolve the conflict of the JSON file correctly now. +- This would be the final Release Candidate. + ## 0.24.0.dev-rc5 ### Improved From a9020a3aea431ab42c37ac893d4a638a85768d27 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 24 Oct 2024 12:41:50 +0100 Subject: [PATCH 7/8] 0.24.0.dev-rc7 --- manifest-beta.json | 2 +- package-lock.json | 4 +- package.json | 2 +- .../ObsidianLiveSyncSettingTab.ts | 108 +++++++++++------- updates.md | 10 ++ 5 files changed, 82 insertions(+), 44 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index 93a389b..1cd4cc4 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.24.0.dev-rc6", + "version": "0.24.0.dev-rc7", "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", diff --git a/package-lock.json b/package-lock.json index 262de85..7d1ed69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc6", + "version": "0.24.0.dev-rc7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc6", + "version": "0.24.0.dev-rc7", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", diff --git a/package.json b/package.json index cbf35e0..dfc3abc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc6", + "version": "0.24.0.dev-rc7", "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", diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 36d1865..47c70bf 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -1,6 +1,6 @@ import { App, PluginSettingTab, MarkdownRenderer, stringifyYaml } from "../../../deps.ts"; import { - DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, LOG_LEVEL_INFO, type LoadedEntry, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, REMOTE_COUCHDB, REMOTE_MINIO, PREFERRED_JOURNAL_SYNC, FLAGMD_REDFLAG, type ConfigLevel, LEVEL_POWER_USER, LEVEL_ADVANCED, LEVEL_EDGE_CASE, type MetaEntry, type FilePath + DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, LOG_LEVEL_INFO, type LoadedEntry, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, REMOTE_COUCHDB, REMOTE_MINIO, PREFERRED_JOURNAL_SYNC, FLAGMD_REDFLAG, type ConfigLevel, LEVEL_POWER_USER, LEVEL_ADVANCED, LEVEL_EDGE_CASE, type MetaEntry, type FilePath, } from "../../../lib/src/common/types.ts"; import { createBlob, delay, isDocContentSame, isObjectDifferent, readAsBlob, sizeToHumanReadable } from "../../../lib/src/common/utils.ts"; import { versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts"; @@ -9,8 +9,8 @@ import { balanceChunkPurgedDBs, checkSyncInfo, isCloudantURI, purgeUnreferencedC import { testCrypt } from "../../../lib/src/encryption/e2ee_v2.ts"; import ObsidianLiveSyncPlugin from "../../../main.ts"; import { getPath, requestToCouchDB, scheduleTask } from "../../../common/utils.ts"; -import { request, TFile } from "obsidian"; -import { shouldBeIgnored } from "../../../lib/src/string_and_binary/path.ts"; +import { request } from "obsidian"; +import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts"; import MultipleRegExpControl from './MultipleRegExpControl.svelte'; import { LiveSyncCouchDBReplicator } from "../../../lib/src/replication/couchdb/LiveSyncReplicator.ts"; import { type AllSettingItemKey, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey, type AllSettings, OnDialogSettingsDefault, type OnDialogSettings, getConfName } from "./settingConstants.ts"; @@ -23,6 +23,8 @@ import { confirmWithMessage } from "../../coreObsidian/UILib/dialogs.ts"; import { EVENT_REQUEST_COPY_SETUP_URI, EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, EVENT_REQUEST_OPEN_SETUP_URI, EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_REQUEST_SHOW_HISTORY, eventHub } from "../../../common/events.ts"; import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock"; import { JournalSyncMinio } from "../../../lib/src/replication/journal/objectstore/JournalSyncMinio.ts"; +import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts"; +import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts"; export type OnUpdateResult = { visibility?: boolean, @@ -1867,11 +1869,12 @@ ${stringifyYaml(pluginConfig)}`; void addPanel(paneEl, "Recovery and Repair").then((paneEl) => { - const addResult = (path: string, file: TFile | false, fileOnDB: LoadedEntry | false) => { + const addResult = async (path: string, file: FilePathWithPrefix | false, fileOnDB: LoadedEntry | false) => { + const storageFileStat = file ? await this.plugin.storageAccess.statHidden(file) : null; resultArea.appendChild(this.createEl(resultArea, "div", {}, el => { el.appendChild(this.createEl(el, "h6", { text: path })); el.appendChild(this.createEl(el, "div", {}, infoGroupEl => { - infoGroupEl.appendChild(this.createEl(infoGroupEl, "div", { text: `Storage : Modified: ${!file ? `Missing:` : `${new Date(file.stat.mtime).toLocaleString()}, Size:${file.stat.size}`}` })) + infoGroupEl.appendChild(this.createEl(infoGroupEl, "div", { text: `Storage : Modified: ${!storageFileStat ? `Missing:` : `${new Date(storageFileStat.mtime).toLocaleString()}, Size:${storageFileStat.size}`}` })) infoGroupEl.appendChild(this.createEl(infoGroupEl, "div", { text: `Database: Modified: ${!fileOnDB ? `Missing:` : `${new Date(fileOnDB.mtime).toLocaleString()}, Size:${fileOnDB.size}`}` })) })); if (fileOnDB && file) { @@ -1886,20 +1889,24 @@ ${stringifyYaml(pluginConfig)}`; if (file) { el.appendChild(this.createEl(el, "button", { text: "Storage -> Database" }, buttonEl => { buttonEl.onClickEvent(async () => { - // const file = this.plugin.storageAccess.getFileStub(path); - // if (!file) { - // Logger(`File not found: ${path}`, LOG_LEVEL_NOTICE); - // return; - // } - // const content = await this.plugin.storageAccess.readStubContent(file); - // if (!content) { - // Logger(`Content cannot be read: ${path}`, LOG_LEVEL_NOTICE); - // return; - // } - // this.plugin.databaseFileAccess.store(content, true); - if (!await this.plugin.fileHandler.storeFileToDB(file.path as FilePath, true)) { - Logger(`Failed to store the file to the database: ${file.path}`, LOG_LEVEL_NOTICE); - return; + if (file.startsWith(".")) { + const addOn = this.plugin.getAddOn(HiddenFileSync.name); + if (addOn) { + const file = (await addOn.scanInternalFiles()).find(e => e.path == path); + if (!file) { + Logger(`Failed to find the file in the internal files: ${path}`, LOG_LEVEL_NOTICE); + return; + } + if (!await addOn.storeInternalFileToDatabase(file, true)) { + Logger(`Failed to store the file to the database (Hidden file): ${file}`, LOG_LEVEL_NOTICE); + return; + } + } + } else { + if (!await this.plugin.fileHandler.storeFileToDB(file as FilePath, true)) { + Logger(`Failed to store the file to the database: ${file}`, LOG_LEVEL_NOTICE); + return; + } } el.remove(); }) @@ -1908,10 +1915,20 @@ ${stringifyYaml(pluginConfig)}`; if (fileOnDB) { el.appendChild(this.createEl(el, "button", { text: "Database -> Storage" }, buttonEl => { buttonEl.onClickEvent(async () => { - // this.plugin.pullFile(this.plugin.getPath(fileOnDB), undefined, true, undefined, false); - if (!await this.plugin.fileHandler.dbToStorage(fileOnDB as MetaEntry, null, true)) { - Logger(`Failed to store the file to the storage: ${fileOnDB.path}`, LOG_LEVEL_NOTICE); - return; + if (fileOnDB.path.startsWith(ICHeader)) { + const addOn = this.plugin.getAddOn(HiddenFileSync.name); + if (addOn) { + if (!await addOn.extractInternalFileFromDatabase(path as FilePath, true)) { + Logger(`Failed to store the file to the database (Hidden file): ${file}`, LOG_LEVEL_NOTICE); + return; + } + } + + } else { + if (!await this.plugin.fileHandler.dbToStorage(fileOnDB as MetaEntry, null, true)) { + Logger(`Failed to store the file to the storage: ${fileOnDB.path}`, LOG_LEVEL_NOTICE); + return; + } } el.remove(); }) @@ -1921,14 +1938,14 @@ ${stringifyYaml(pluginConfig)}`; })) } - const checkBetweenStorageAndDatabase = async (file: TFile, fileOnDB: LoadedEntry) => { + const checkBetweenStorageAndDatabase = async (file: FilePathWithPrefix, fileOnDB: LoadedEntry) => { const dataContent = readAsBlob(fileOnDB); - const content = createBlob(await this.plugin.storageAccess.readFileAuto(file.path)) + const content = createBlob(await this.plugin.storageAccess.readHiddenFileBinary(file)) if (await isDocContentSame(content, dataContent)) { - Logger(`Compare: SAME: ${file.path}`) + Logger(`Compare: SAME: ${file}`) } else { - Logger(`Compare: CONTENT IS NOT MATCHED! ${file.path}`, LOG_LEVEL_NOTICE); - addResult(file.path, file, fileOnDB) + Logger(`Compare: CONTENT IS NOT MATCHED! ${file}`, LOG_LEVEL_NOTICE); + void addResult(file, file, fileOnDB) } } new Setting(paneEl) @@ -1956,17 +1973,26 @@ ${stringifyYaml(pluginConfig)}`; .setDisabled(false) .setCta() .onClick(async () => { + Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); + const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns + .replace(/\n| /g, "") + .split(",").filter(e => e).map(e => new RegExp(e, "i")); this.plugin.localDatabase.hashCaches.clear(); Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); - const files = this.app.vault.getFiles(); - const documents = [] as FilePathWithPrefix[]; + const files = await this.plugin.storageAccess.getFilesIncludeHidden("/", undefined, ignorePatterns) + const documents = [] as FilePath[]; - const adn = this.plugin.localDatabase.findAllNormalDocs() - for await (const i of adn) documents.push(getPath(i)); + const adn = this.plugin.localDatabase.findAllDocs() + for await (const i of adn) { + const path = getPath(i); + if (path.startsWith(ICXHeader)) continue; + if (path.startsWith(PSCHeader)) continue; + documents.push(stripAllPrefixes(path)); + } const allPaths = [ ...new Set([ ...documents, - ...files.map(e => e.path as FilePathWithPrefix)])]; + ...files])]; let i = 0; const incProc = () => { i++; @@ -1978,27 +2004,29 @@ ${stringifyYaml(pluginConfig)}`; if (shouldBeIgnored(path)) { return incProc(); } - const abstractFile = this.plugin.storageAccess.getFileStub(path); - const fileOnStorage = abstractFile instanceof TFile ? abstractFile : false; + const stat = await this.plugin.storageAccess.isExistsIncludeHidden(path) ? await this.plugin.storageAccess.statHidden(path) : false; + const fileOnStorage = stat != null ? stat : false; if (!await this.plugin.$$isTargetFile(path)) return incProc(); const releaser = await semaphore.acquire(1) - if (fileOnStorage && this.plugin.$$isFileSizeExceeded(fileOnStorage.stat.size)) return incProc(); + if (fileOnStorage && this.plugin.$$isFileSizeExceeded(fileOnStorage.size)) return incProc(); try { - const fileOnDB = await this.plugin.localDatabase.getDBEntry(path); + const isHiddenFile = path.startsWith("."); + const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path; + const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath); if (fileOnDB && this.plugin.$$isFileSizeExceeded(fileOnDB.size)) return incProc(); if (!fileOnDB && fileOnStorage) { Logger(`Compare: Not found on the local database: ${path}`, LOG_LEVEL_NOTICE); - addResult(path, fileOnStorage, false) + void addResult(path, path, false) return incProc(); } if (fileOnDB && !fileOnStorage) { Logger(`Compare: Not found on the storage: ${path}`, LOG_LEVEL_NOTICE); - addResult(path, false, fileOnDB) + void addResult(path, false, fileOnDB) return incProc(); } if (fileOnStorage && fileOnDB) { - await checkBetweenStorageAndDatabase(fileOnStorage, fileOnDB) + await checkBetweenStorageAndDatabase(path, fileOnDB) } } catch (ex) { Logger(`Error while processing ${path}`, LOG_LEVEL_NOTICE); diff --git a/updates.md b/updates.md index 56ff6a9..f023d80 100644 --- a/updates.md +++ b/updates.md @@ -23,6 +23,16 @@ Thank you, and I hope your troubles will be resolved! --- +## 0.24.0.dev-rc7 + +### Fixed + +- Verifying files between the local database and storage is now working correctly. + +### New Features + +- We can verify and resolve also the hidden files now. + ## 0.24.0.dev-rc6 ### Fixed From 5afe24c4604c9be9cc472f9e38f424d4a5d2b32e Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 25 Oct 2024 12:39:32 +0100 Subject: [PATCH 8/8] 0.24.0.dev-rc8 --- manifest-beta.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- .../HiddenFileSync/CmdHiddenFileSync.ts | 19 +++++++++---------- updates.md | 6 ++++++ 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index 1cd4cc4..99ddb1b 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.24.0.dev-rc7", + "version": "0.24.0.dev-rc8", "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", diff --git a/package-lock.json b/package-lock.json index 7d1ed69..7eaa799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc7", + "version": "0.24.0.dev-rc8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc7", + "version": "0.24.0.dev-rc8", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.645.0", diff --git a/package.json b/package.json index dfc3abc..2d66f3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.24.0.dev-rc7", + "version": "0.24.0.dev-rc8", "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", diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index 6e24a43..d57699a 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -407,7 +407,8 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule fileOnStorage: xFileOnStorage, fileOnDatabase: xFileOnDatabase } = params[0]; - if (xFileOnStorage && xFileOnDatabase) { + const xFileOnDatabaseExists = xFileOnDatabase !== undefined && !(xFileOnDatabase.deleted || xFileOnDatabase._deleted); + if (xFileOnStorage && xFileOnDatabaseExists) { // Both => Synchronize if ((direction != "pullForce" && direction != "pushForce") && isMarkedAsSameChanges(filename, [xFileOnDatabase.mtime, xFileOnStorage.mtime]) == EVEN) { this._log(`Hidden file skipped: ${filename} is marked as same`, LOG_LEVEL_VERBOSE); @@ -426,7 +427,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } else { // Even, or not forced. skip. } - } else if (!xFileOnStorage && xFileOnDatabase) { + } else if (!xFileOnStorage && xFileOnDatabaseExists) { if (direction == "push" || direction == "pushForce") { if (xFileOnDatabase.deleted) return; @@ -442,13 +443,14 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule countUpdatedFolder(filename); } } - } else if (xFileOnStorage && !xFileOnDatabase) { + } else if (xFileOnStorage && !xFileOnDatabaseExists) { if (direction == "push" || direction == "pushForce" || direction == "safe") { await this.storeInternalFileToDatabase(xFileOnStorage); } else { - // if (await this.extractInternalFileFromDatabase(xFileOnStorage.path)) { - // countUpdatedFolder(xFileOnStorage.path); - // } + // Apply the deletion + if (await this.extractInternalFileFromDatabase(xFileOnStorage.path)) { + countUpdatedFolder(xFileOnStorage.path); + } } } else { throw new Error("Invalid state on hidden file sync"); @@ -676,8 +678,6 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule this._log(`STORAGE