Compare commits

...

21 Commits

Author SHA1 Message Date
vorotamoroz
f0ffb0620e Fixed file deletion failures. 2022-07-29 13:33:33 +09:00
vorotamoroz
c88c939cd9 bumped 2022-07-29 09:42:22 +09:00
vorotamoroz
05b53eb2cf Fixes:
- Now conflict resolution back well
2022-07-29 09:38:23 +09:00
vorotamoroz
61b65b0461 Bumped 2022-07-28 19:20:59 +09:00
vorotamoroz
ac9be937b4 New feature:
- The metadata of the deleted files will be kept on the database by default.
- We can see the history of deleted files.
- Update information became to be shown on the major upgrade.

Fixed:
- `Pick file to show` was renamed to `Pick a file to show.
- Files in the `Pick a file to show` are now ordered by their modified date descent.
2022-07-28 19:19:37 +09:00
vorotamoroz
c610284cab bumped again 2022-07-25 13:50:42 +09:00
vorotamoroz
2e6ed4777c Added postcss for ci 2022-07-25 13:22:37 +09:00
vorotamoroz
ab6ff01f1a Bumped 2022-07-25 13:16:49 +09:00
vorotamoroz
c836953fa9 Fixed:
- Leaked prototyping code.

    New feature:
    - `Pick file to show history` shows a file list and you can choose one for showing history.
    - `Back to this revision` implemented on the Document history dialog.
2022-07-25 13:14:59 +09:00
vorotamoroz
e69371ff24 Degraded! 2022-07-25 13:05:00 +09:00
vorotamoroz
d324add086 Fixed:
- Leaked prototyping code.

New feature:
- `Pick file to show history` shows a file list and you can choose one for showing history.
- `Back to this revision` implemented on the Document history dialog.
2022-07-25 12:48:48 +09:00
vorotamoroz
0caf330f39 Merge remote-tracking branch 'origin/snyk-upgrade-52349e72cdcadcd0ba41dd967c715aa7' 2022-07-21 16:40:18 +09:00
vorotamoroz
3a147ca427 Merge branch 'snyk-upgrade-6a3786fc4cbe394542e9d1b6058c46e7' 2022-07-21 16:26:54 +09:00
vorotamoroz
8266cfba40 Merge remote-tracking branch 'origin/snyk-upgrade-d6d88a887d6bc2d451a6e9c7a7759727' 2022-07-21 16:15:35 +09:00
vorotamoroz
e2f06181fa bumped 2022-07-21 13:06:57 +09:00
vorotamoroz
bb6d787607 Imprinting version numbers to boot log. 2022-07-21 13:06:42 +09:00
vorotamoroz
cb406e2db6 New feature.
- Local database name can now be customized.
- Buttons to back skip-patterns of Hidden file sync to default.
2022-07-21 13:05:35 +09:00
vorotamoroz
0a1248c5fc Fixed:
- Now Notification is less noisy.
- Some synchronization won't be missed.
- Scanning speed improved.

Implemented:

- Implemented notifications to reload the plugin.
- Rescue button to updating all hidden files for overwriting them on other vaults.
2022-07-20 16:57:21 +09:00
snyk-bot
d5a95d43dd fix: upgrade idb from 7.0.1 to 7.0.2
Snyk has created this PR to upgrade idb from 7.0.1 to 7.0.2.

See this package in npm:
https://www.npmjs.com/package/idb

See this project in Snyk:
https://app.snyk.io/org/vrtmrz/project/d2c9b72d-6e38-433f-bbad-725719c0fa4d?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-07-09 05:02:39 +00:00
snyk-bot
7da930a8bb fix: upgrade svelte-preprocess from 4.10.3 to 4.10.5
Snyk has created this PR to upgrade svelte-preprocess from 4.10.3 to 4.10.5.

See this package in npm:
https://www.npmjs.com/package/svelte-preprocess

See this project in Snyk:
https://app.snyk.io/org/vrtmrz/project/d2c9b72d-6e38-433f-bbad-725719c0fa4d?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-04-29 03:54:29 +00:00
snyk-bot
a632b79726 fix: upgrade esbuild-svelte from 0.6.2 to 0.7.0
Snyk has created this PR to upgrade esbuild-svelte from 0.6.2 to 0.7.0.

See this package in npm:
https://www.npmjs.com/package/esbuild-svelte

See this project in Snyk:
https://app.snyk.io/org/vrtmrz/project/d2c9b72d-6e38-433f-bbad-725719c0fa4d?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-04-29 03:54:25 +00:00
13 changed files with 973 additions and 282 deletions

View File

@@ -3,7 +3,7 @@ import process from "process";
import builtins from "builtin-modules"; import builtins from "builtin-modules";
import sveltePlugin from "esbuild-svelte"; import sveltePlugin from "esbuild-svelte";
import sveltePreprocess from "svelte-preprocess"; import sveltePreprocess from "svelte-preprocess";
import fs from "node:fs";
const banner = `/* const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin if you want to view the source, please visit the github repository of this plugin
@@ -11,7 +11,9 @@ if you want to view the source, please visit the github repository of this plugi
`; `;
const prod = process.argv[2] === "production"; const prod = process.argv[2] === "production";
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json"));
const packageJson = JSON.parse(fs.readFileSync("./package.json"));
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
esbuild esbuild
.build({ .build({
banner: { banner: {
@@ -19,6 +21,11 @@ esbuild
}, },
entryPoints: ["src/main.ts"], entryPoints: ["src/main.ts"],
bundle: true, bundle: true,
define: {
"MANIFEST_VERSION": `"${manifestJson.version}"`,
"PACKAGE_VERSION": `"${packageJson.version}"`,
"UPDATE_INFO": `${updateInfo}`,
},
external: ["obsidian", "electron", ...builtins], external: ["obsidian", "electron", ...builtins],
format: "cjs", format: "cjs",
watch: !prod, watch: !prod,

View File

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

225
package-lock.json generated
View File

@@ -1,19 +1,18 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.12.0", "version": "0.13.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.12.0", "version": "0.13.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"esbuild": "0.13.12", "esbuild": "0.13.12",
"esbuild-svelte": "^0.6.0", "esbuild-svelte": "^0.7.0",
"idb": "^7.0.1", "idb": "^7.0.2",
"svelte-preprocess": "^4.10.2",
"xxhash-wasm": "^0.4.2" "xxhash-wasm": "^0.4.2"
}, },
"devDependencies": { "devDependencies": {
@@ -27,13 +26,16 @@
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
"builtin-modules": "^3.2.0", "builtin-modules": "^3.2.0",
"esbuild": "0.13.12", "esbuild": "0.13.12",
"esbuild-svelte": "^0.6.0", "esbuild-svelte": "^0.7.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.25.2", "eslint-plugin-import": "^2.25.2",
"obsidian": "^0.15.4", "obsidian": "^0.15.4",
"postcss": "^8.4.14",
"postcss-load-config": "^3.1.4",
"rollup": "^2.32.1", "rollup": "^2.32.1",
"svelte-preprocess": "^4.10.2", "svelte": "^3.49.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.2.0", "tslib": "^2.2.0",
"typescript": "^4.2.4" "typescript": "^4.2.4"
} }
@@ -1416,18 +1418,16 @@
] ]
}, },
"node_modules/esbuild-svelte": { "node_modules/esbuild-svelte": {
"version": "0.6.2", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.6.2.tgz", "resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.7.0.tgz",
"integrity": "sha512-FRHcyaQqIm4ncFsbk97b+80fHAI0VA15Ty56zOai9zpOPOQ1kgqWJt7JYn0jNGm+1VSvJNaGUj8QB85H/P43jA==", "integrity": "sha512-hfiauhEXtGocUf7oVcxTrLhhF57ajBbvNCCClsS3KntEeITddKU+1WFhmsCt9SO0dQJlCFzJtpPu2dI7dRkXBw==",
"dev": true, "dev": true,
"dependencies": {
"svelte": "^3.46.2"
},
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
"peerDependencies": { "peerDependencies": {
"esbuild": ">=0.9.6" "esbuild": ">=0.9.6",
"svelte": ">=3.43.0"
} }
}, },
"node_modules/esbuild-windows-32": { "node_modules/esbuild-windows-32": {
@@ -2109,9 +2109,9 @@
} }
}, },
"node_modules/idb": { "node_modules/idb": {
"version": "7.0.1", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.2.tgz",
"integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" "integrity": "sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg=="
}, },
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.1.9", "version": "5.1.9",
@@ -2454,6 +2454,15 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lilconfig": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
"integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
@@ -2576,6 +2585,18 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -2776,6 +2797,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
@@ -2800,6 +2827,59 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-load-config": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
"dev": true,
"dependencies": {
"lilconfig": "^2.0.5",
"yaml": "^1.10.2"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"postcss": {
"optional": true
},
"ts-node": {
"optional": true
}
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -3068,6 +3148,15 @@
"sorcery": "bin/index.js" "sorcery": "bin/index.js"
} }
}, },
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sourcemap-codec": { "node_modules/sourcemap-codec": {
"version": "1.4.8", "version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
@@ -3185,18 +3274,18 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "3.46.4", "version": "3.49.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.46.4.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz",
"integrity": "sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg==", "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/svelte-preprocess": { "node_modules/svelte-preprocess": {
"version": "4.10.3", "version": "4.10.7",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.3.tgz", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz",
"integrity": "sha512-ttw17lJfb/dx2ZJT9sesaXT5l7mPQ9Apx1H496Kli3Hkk7orIRGpOw6rCPkRNzr6ueVPqb4vzodS5x7sBFhKHw==", "integrity": "sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
@@ -3215,7 +3304,7 @@
"coffeescript": "^2.5.1", "coffeescript": "^2.5.1",
"less": "^3.11.3 || ^4.0.0", "less": "^3.11.3 || ^4.0.0",
"postcss": "^7 || ^8", "postcss": "^7 || ^8",
"postcss-load-config": "^2.1.0 || ^3.0.0", "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0",
"pug": "^3.0.0", "pug": "^3.0.0",
"sass": "^1.26.8", "sass": "^1.26.8",
"stylus": "^0.55.0", "stylus": "^0.55.0",
@@ -3484,6 +3573,15 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true "dev": true
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"engines": {
"node": ">= 6"
}
} }
}, },
"dependencies": { "dependencies": {
@@ -4526,13 +4624,11 @@
"optional": true "optional": true
}, },
"esbuild-svelte": { "esbuild-svelte": {
"version": "0.6.2", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.6.2.tgz", "resolved": "https://registry.npmjs.org/esbuild-svelte/-/esbuild-svelte-0.7.0.tgz",
"integrity": "sha512-FRHcyaQqIm4ncFsbk97b+80fHAI0VA15Ty56zOai9zpOPOQ1kgqWJt7JYn0jNGm+1VSvJNaGUj8QB85H/P43jA==", "integrity": "sha512-hfiauhEXtGocUf7oVcxTrLhhF57ajBbvNCCClsS3KntEeITddKU+1WFhmsCt9SO0dQJlCFzJtpPu2dI7dRkXBw==",
"dev": true, "dev": true,
"requires": { "requires": {}
"svelte": "^3.46.2"
}
}, },
"esbuild-windows-32": { "esbuild-windows-32": {
"version": "0.13.12", "version": "0.13.12",
@@ -5052,9 +5148,9 @@
} }
}, },
"idb": { "idb": {
"version": "7.0.1", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.2.tgz",
"integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" "integrity": "sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg=="
}, },
"ignore": { "ignore": {
"version": "5.1.9", "version": "5.1.9",
@@ -5301,6 +5397,12 @@
"type-check": "~0.4.0" "type-check": "~0.4.0"
} }
}, },
"lilconfig": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
"integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
"dev": true
},
"locate-path": { "locate-path": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
@@ -5399,6 +5501,12 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true
},
"natural-compare": { "natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -5547,6 +5655,12 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true "dev": true
}, },
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"picomatch": { "picomatch": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
@@ -5562,6 +5676,27 @@
"find-up": "^2.1.0" "find-up": "^2.1.0"
} }
}, },
"postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
"dev": true,
"requires": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
},
"postcss-load-config": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
"dev": true,
"requires": {
"lilconfig": "^2.0.5",
"yaml": "^1.10.2"
}
},
"prelude-ls": { "prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5734,6 +5869,12 @@
"sourcemap-codec": "^1.3.0" "sourcemap-codec": "^1.3.0"
} }
}, },
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"sourcemap-codec": { "sourcemap-codec": {
"version": "1.4.8", "version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
@@ -5824,15 +5965,15 @@
} }
}, },
"svelte": { "svelte": {
"version": "3.46.4", "version": "3.49.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.46.4.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz",
"integrity": "sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg==", "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==",
"dev": true "dev": true
}, },
"svelte-preprocess": { "svelte-preprocess": {
"version": "4.10.3", "version": "4.10.7",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.3.tgz", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz",
"integrity": "sha512-ttw17lJfb/dx2ZJT9sesaXT5l7mPQ9Apx1H496Kli3Hkk7orIRGpOw6rCPkRNzr6ueVPqb4vzodS5x7sBFhKHw==", "integrity": "sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/pug": "^2.0.4", "@types/pug": "^2.0.4",
@@ -6025,6 +6166,12 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true "dev": true
},
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.12.0", "version": "0.13.2",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js", "main": "main.js",
"type": "module", "type": "module",
@@ -23,22 +23,24 @@
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
"builtin-modules": "^3.2.0", "builtin-modules": "^3.2.0",
"esbuild": "0.13.12", "esbuild": "0.13.12",
"esbuild-svelte": "^0.6.0", "esbuild-svelte": "^0.7.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.25.2", "eslint-plugin-import": "^2.25.2",
"obsidian": "^0.15.4", "obsidian": "^0.15.4",
"postcss": "^8.4.14",
"postcss-load-config": "^3.1.4",
"rollup": "^2.32.1", "rollup": "^2.32.1",
"svelte-preprocess": "^4.10.2", "svelte": "^3.49.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.2.0", "tslib": "^2.2.0",
"typescript": "^4.2.4" "typescript": "^4.2.4"
}, },
"dependencies": { "dependencies": {
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"esbuild": "0.13.12", "esbuild": "0.13.12",
"esbuild-svelte": "^0.6.0", "esbuild-svelte": "^0.7.0",
"idb": "^7.0.1", "idb": "^7.0.2",
"svelte-preprocess": "^4.10.2",
"xxhash-wasm": "^0.4.2" "xxhash-wasm": "^0.4.2"
} }
} }

View File

@@ -1,9 +1,9 @@
import { TFile, Modal, App } from "obsidian"; import { TFile, Modal, App } from "obsidian";
import { path2id } from "./utils"; import { path2id } from "./utils";
import { escapeStringToHTML } from "./lib/src/utils"; import { base64ToArrayBuffer, base64ToString, escapeStringToHTML, isValidPath } from "./lib/src/utils";
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import { LOG_LEVEL } from "./lib/src/types"; import { LoadedEntry, LOG_LEVEL } from "./lib/src/types";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
export class DocumentHistoryModal extends Modal { export class DocumentHistoryModal extends Modal {
@@ -17,12 +17,14 @@ export class DocumentHistoryModal extends Modal {
file: string; file: string;
revs_info: PouchDB.Core.RevisionInfo[] = []; revs_info: PouchDB.Core.RevisionInfo[] = [];
currentDoc: LoadedEntry;
currentText = ""; currentText = "";
currentDeleted = false;
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile) { constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | string) {
super(app); super(app);
this.plugin = plugin; this.plugin = plugin;
this.file = file.path; this.file = (file instanceof TFile) ? file.path : file;
if (localStorage.getItem("ols-history-highlightdiff") == "1") { if (localStorage.getItem("ols-history-highlightdiff") == "1") {
this.showDiff = true; this.showDiff = true;
} }
@@ -40,24 +42,29 @@ export class DocumentHistoryModal extends Modal {
const db = this.plugin.localDatabase; const db = this.plugin.localDatabase;
const index = this.revs_info.length - 1 - (this.range.value as any) / 1; const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
const rev = this.revs_info[index]; const rev = this.revs_info[index];
const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false); const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false, true);
this.currentText = ""; this.currentText = "";
this.currentDeleted = false;
if (w === false) { if (w === false) {
this.currentDeleted = true;
this.info.innerHTML = ""; this.info.innerHTML = "";
this.contentView.innerHTML = `Could not read this revision<br>(${rev.rev})`; this.contentView.innerHTML = `Could not read this revision<br>(${rev.rev})`;
} else { } else {
this.currentDoc = w;
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`; this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
let result = ""; let result = "";
this.currentText = w.data; const w1data = w.datatype == "plain" ? w.data : base64ToString(w.data);
this.currentDeleted = w.deleted;
this.currentText = w1data;
if (this.showDiff) { if (this.showDiff) {
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1); const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) { if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
const oldRev = this.revs_info[prevRevIdx].rev; const oldRev = this.revs_info[prevRevIdx].rev;
const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false); const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false, true);
if (w2 != false) { if (w2 != false) {
const dmp = new diff_match_patch(); const dmp = new diff_match_patch();
const diff = dmp.diff_main(w2.data, w.data); const w2data = w2.datatype == "plain" ? w2.data : base64ToString(w2.data);
const diff = dmp.diff_main(w2data, w1data);
dmp.diff_cleanupSemantic(diff); dmp.diff_cleanupSemantic(diff);
for (const v of diff) { for (const v of diff) {
const x1 = v[0]; const x1 = v[0];
@@ -73,15 +80,16 @@ export class DocumentHistoryModal extends Modal {
result = result.replace(/\n/g, "<br>"); result = result.replace(/\n/g, "<br>");
} else { } else {
result = escapeStringToHTML(w.data); result = escapeStringToHTML(w1data);
} }
} else { } else {
result = escapeStringToHTML(w.data); result = escapeStringToHTML(w1data);
} }
} else { } else {
result = escapeStringToHTML(w.data); result = escapeStringToHTML(w1data);
} }
this.contentView.innerHTML = result; this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
} }
} }
@@ -138,6 +146,38 @@ export class DocumentHistoryModal extends Modal {
Logger(`Old content copied to clipboard`, LOG_LEVEL.NOTICE); Logger(`Old content copied to clipboard`, LOG_LEVEL.NOTICE);
}); });
}); });
async function focusFile(path: string) {
const targetFile = app.vault
.getFiles()
.find((f) => f.path === path);
if (targetFile) {
const leaf = app.workspace.getLeaf(false);
await leaf.openFile(targetFile);
} else {
Logger("The file cound not view on the editor", LOG_LEVEL.NOTICE)
}
}
buttons.createEl("button", { text: "Back to this revision" }, (e) => {
e.addClass("mod-cta");
e.addEventListener("click", async () => {
const pathToWrite = this.file.startsWith("i:") ? this.file.substring("i:".length) : this.file;
if (!isValidPath(pathToWrite)) {
Logger("Path is not vaild to write content.", LOG_LEVEL.INFO);
}
if (this.currentDoc?.datatype == "plain") {
await this.app.vault.adapter.write(pathToWrite, this.currentDoc.data);
await focusFile(pathToWrite);
this.close();
} else if (this.currentDoc?.datatype == "newnote") {
await this.app.vault.adapter.writeBinary(pathToWrite, base64ToArrayBuffer(this.currentDoc.data));
await focusFile(pathToWrite);
this.close();
} else {
Logger(`Could not parse entry`, LOG_LEVEL.NOTICE);
}
});
});
} }
onClose() { onClose() {
const { contentEl } = this; const { contentEl } = this;

View File

@@ -35,8 +35,8 @@ import { LRUCache } from "./lib/src/LRUCache";
const currentVersionRange: ChunkVersionRange = { const currentVersionRange: ChunkVersionRange = {
min: 0, min: 0,
max: 1, max: 2,
current: 1, current: 2,
} }
type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>; type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>;
@@ -69,7 +69,9 @@ export class LocalPouchDB {
isMobile = false; isMobile = false;
chunkVersion = 0; chunkVersion = -1;
maxChunkVersion = -1;
minChunkVersion = -1;
cancelHandler<T extends PouchDB.Core.Changes<EntryDoc> | PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>>(handler: T): T { cancelHandler<T extends PouchDB.Core.Changes<EntryDoc> | PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>>(handler: T): T {
if (handler != null) { if (handler != null) {
@@ -296,7 +298,7 @@ export class LocalPouchDB {
} }
} }
async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> { async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions, includeDeleted = false): Promise<false | LoadedEntry> {
const id = path2id(path); const id = path2id(path);
try { try {
let obj: EntryDocResponse = null; let obj: EntryDocResponse = null;
@@ -306,6 +308,7 @@ export class LocalPouchDB {
obj = await this.localDatabase.get(id); obj = await this.localDatabase.get(id);
} }
const deleted = "deleted" in obj ? obj.deleted : undefined; const deleted = "deleted" in obj ? obj.deleted : undefined;
if (!includeDeleted && deleted) return false;
if (obj.type && obj.type == "leaf") { if (obj.type && obj.type == "leaf") {
//do nothing for leaf; //do nothing for leaf;
return false; return false;
@@ -326,7 +329,7 @@ export class LocalPouchDB {
ctime: note.ctime, ctime: note.ctime,
mtime: note.mtime, mtime: note.mtime,
size: note.size, size: note.size,
_deleted: obj._deleted, // _deleted: obj._deleted,
_rev: obj._rev, _rev: obj._rev,
_conflicts: obj._conflicts, _conflicts: obj._conflicts,
children: children, children: children,
@@ -344,7 +347,7 @@ export class LocalPouchDB {
} }
return false; return false;
} }
async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true): Promise<false | LoadedEntry> { async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true, includeDeleted = false): Promise<false | LoadedEntry> {
const id = path2id(path); const id = path2id(path);
try { try {
let obj: EntryDocResponse = null; let obj: EntryDocResponse = null;
@@ -354,7 +357,7 @@ export class LocalPouchDB {
obj = await this.localDatabase.get(id); obj = await this.localDatabase.get(id);
} }
const deleted = "deleted" in obj ? obj.deleted : undefined; const deleted = "deleted" in obj ? obj.deleted : undefined;
if (!includeDeleted && deleted) return false;
if (obj.type && obj.type == "leaf") { if (obj.type && obj.type == "leaf") {
//do nothing for leaf; //do nothing for leaf;
return false; return false;
@@ -369,7 +372,7 @@ export class LocalPouchDB {
ctime: note.ctime, ctime: note.ctime,
mtime: note.mtime, mtime: note.mtime,
size: note.size, size: note.size,
_deleted: obj._deleted, // _deleted: obj._deleted,
_rev: obj._rev, _rev: obj._rev,
_conflicts: obj._conflicts, _conflicts: obj._conflicts,
children: [], children: [],
@@ -415,7 +418,7 @@ export class LocalPouchDB {
ctime: obj.ctime, ctime: obj.ctime,
mtime: obj.mtime, mtime: obj.mtime,
size: obj.size, size: obj.size,
_deleted: obj._deleted, // _deleted: obj._deleted,
_rev: obj._rev, _rev: obj._rev,
children: obj.children, children: obj.children,
datatype: obj.type, datatype: obj.type,
@@ -459,6 +462,7 @@ export class LocalPouchDB {
} else { } else {
obj = await this.localDatabase.get(id); obj = await this.localDatabase.get(id);
} }
const revDeletion = opt && (("rev" in opt ? opt.rev : "") != "");
if (obj.type && obj.type == "leaf") { if (obj.type && obj.type == "leaf") {
//do nothing for leaf; //do nothing for leaf;
@@ -476,7 +480,15 @@ export class LocalPouchDB {
// simple note // simple note
} }
if (obj.type == "newnote" || obj.type == "plain") { if (obj.type == "newnote" || obj.type == "plain") {
obj._deleted = true; if (revDeletion) {
obj._deleted = true;
} else {
obj.deleted = true;
obj.mtime = Date.now();
if (this.settings.deleteMetadataOfDeletedFiles) {
obj._deleted = true;
}
}
const r = await this.localDatabase.put(obj); const r = await this.localDatabase.put(obj);
Logger(`entry removed:${obj._id}-${r.rev}`); Logger(`entry removed:${obj._id}-${r.rev}`);
if (typeof this.corruptedEntries[obj._id] != "undefined") { if (typeof this.corruptedEntries[obj._id] != "undefined") {
@@ -528,7 +540,15 @@ export class LocalPouchDB {
try { try {
await runWithLock("file:" + v, false, async () => { await runWithLock("file:" + v, false, async () => {
const item = await this.localDatabase.get(v); const item = await this.localDatabase.get(v);
item._deleted = true; if (item.type == "newnote" || item.type == "plain") {
item.deleted = true;
if (this.settings.deleteMetadataOfDeletedFiles) {
item._deleted = true;
}
item.mtime = Date.now();
} else {
item._deleted = true;
}
await this.localDatabase.put(item); await this.localDatabase.put(item);
}); });
@@ -777,16 +797,31 @@ export class LocalPouchDB {
remoteMilestone.node_chunk_info = { ...defMilestonePoint.node_chunk_info, ...remoteMilestone.node_chunk_info }; remoteMilestone.node_chunk_info = { ...defMilestonePoint.node_chunk_info, ...remoteMilestone.node_chunk_info };
this.remoteLocked = remoteMilestone.locked; this.remoteLocked = remoteMilestone.locked;
this.remoteLockedAndDeviceNotAccepted = remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1; this.remoteLockedAndDeviceNotAccepted = remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1;
const writeMilestone = ((remoteMilestone.node_chunk_info[this.nodeid].min != currentVersionRange.min || remoteMilestone.node_chunk_info[this.nodeid].max != currentVersionRange.max) const writeMilestone = (
(
remoteMilestone.node_chunk_info[this.nodeid].min != currentVersionRange.min
|| remoteMilestone.node_chunk_info[this.nodeid].max != currentVersionRange.max
)
|| typeof remoteMilestone._rev == "undefined"); || typeof remoteMilestone._rev == "undefined");
if (writeMilestone) { if (writeMilestone) {
remoteMilestone.node_chunk_info[this.nodeid].min = currentVersionRange.min;
remoteMilestone.node_chunk_info[this.nodeid].max = currentVersionRange.max;
await dbret.db.put(remoteMilestone); await dbret.db.put(remoteMilestone);
} }
// Check compatibility and make sure available version
//
// v min of A v max of A
// | v min of B | v max of B
// | | | |
// | |<--- We can use --->| |
// | | | |
//If globalMin and globalMax is suitable, we can upgrade.
let globalMin = currentVersionRange.min; let globalMin = currentVersionRange.min;
let globalMax = currentVersionRange.max; let globalMax = currentVersionRange.max;
for (const nodeid of remoteMilestone.accepted_nodes) { for (const nodeid of remoteMilestone.accepted_nodes) {
if (nodeid == this.nodeid) continue;
if (nodeid in remoteMilestone.node_chunk_info) { if (nodeid in remoteMilestone.node_chunk_info) {
const nodeinfo = remoteMilestone.node_chunk_info[nodeid]; const nodeinfo = remoteMilestone.node_chunk_info[nodeid];
globalMin = Math.max(nodeinfo.min, globalMin); globalMin = Math.max(nodeinfo.min, globalMin);
@@ -796,7 +831,15 @@ export class LocalPouchDB {
globalMax = 0; globalMax = 0;
} }
} }
//If globalMin and globalMax is suitable, we can upgrade. this.maxChunkVersion = globalMax;
this.minChunkVersion = globalMin;
if (this.chunkVersion >= 0 && (globalMin > this.chunkVersion || globalMax < this.chunkVersion)) {
if (!setting.ignoreVersionCheck) {
Logger("The remote database has no compatibility with the running version. Please upgrade the plugin.", LOG_LEVEL.NOTICE);
return false;
}
}
if (remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1) { if (remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1) {
Logger("The remote database has been rebuilt or corrupted since we have synchronized last time. Fetch rebuilt DB or explicit unlocking is required. See the settings dialog.", LOG_LEVEL.NOTICE); Logger("The remote database has been rebuilt or corrupted since we have synchronized last time. Fetch rebuilt DB or explicit unlocking is required. See the settings dialog.", LOG_LEVEL.NOTICE);
@@ -1242,4 +1285,12 @@ export class LocalPouchDB {
}); });
return; return;
} }
isVersionUpgradable(ver: number) {
if (this.maxChunkVersion < 0) return false;
if (this.minChunkVersion < 0) return false;
if (this.maxChunkVersion > 0 && this.maxChunkVersion < ver) return false;
if (this.minChunkVersion > 0 && this.minChunkVersion > ver) return false;
return true;
}
} }

View File

@@ -1,7 +1,7 @@
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl } from "obsidian"; import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer } from "obsidian";
import { EntryDoc, LOG_LEVEL, RemoteDBSettings } from "./lib/src/types"; import { EntryDoc, LOG_LEVEL, RemoteDBSettings } from "./lib/src/types";
import { path2id, id2path } from "./utils"; import { path2id, id2path } from "./utils";
import { delay, runWithLock } from "./lib/src/utils"; import { delay, runWithLock, versionNumberString2Number } from "./lib/src/utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { checkSyncInfo, connectRemoteCouchDBWithSetting } from "./utils_couchdb"; import { checkSyncInfo, connectRemoteCouchDBWithSetting } from "./utils_couchdb";
import { testCrypt } from "./lib/src/e2ee_v2"; import { testCrypt } from "./lib/src/e2ee_v2";
@@ -39,7 +39,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}; };
w.addClass("sls-setting-menu"); w.addClass("sls-setting-menu");
w.innerHTML = ` w.innerHTML = `
<label class='sls-setting-label selected'><input type='radio' name='disp' value='0' class='sls-setting-tab' checked><div class='sls-setting-menu-btn'>🛰️</div></label> <label class='sls-setting-label selected'><input type='radio' name='disp' value='100' class='sls-setting-tab' checked><div class='sls-setting-menu-btn'>💬</div></label>
<label class='sls-setting-label'><input type='radio' name='disp' value='0' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🛰️</div></label>
<label class='sls-setting-label'><input type='radio' name='disp' value='10' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>📦</div></label> <label class='sls-setting-label'><input type='radio' name='disp' value='10' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>📦</div></label>
<label class='sls-setting-label'><input type='radio' name='disp' value='20' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>⚙️</div></label> <label class='sls-setting-label'><input type='radio' name='disp' value='20' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>⚙️</div></label>
<label class='sls-setting-label'><input type='radio' name='disp' value='30' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔁</div></label> <label class='sls-setting-label'><input type='radio' name='disp' value='30' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔁</div></label>
@@ -68,6 +69,34 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}); });
}); });
const containerInformationEl = containerEl.createDiv();
const h3El = containerInformationEl.createEl("h3", { text: "Updates" });
const informationDivEl = containerInformationEl.createEl("div", { text: "" });
//@ts-ignore
const manifestVersion: string = MANIFEST_VERSION || "-";
//@ts-ignore
const updateInformation: string = UPDATE_INFO || "";
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
const tmpDiv = createSpan();
tmpDiv.addClass("sls-header-button");
tmpDiv.innerHTML = `<button> OK, I read all. </button>`;
if (lastVersion > this.plugin.settings.lastReadUpdates) {
const informationButtonDiv = h3El.appendChild(tmpDiv);
informationButtonDiv.querySelector("button").addEventListener("click", async () => {
this.plugin.settings.lastReadUpdates = lastVersion;
await this.plugin.saveSettings();
informationButtonDiv.remove();
});
}
MarkdownRenderer.renderMarkdown(updateInformation, informationDivEl, "/", null);
addScreenElement("100", containerInformationEl);
const containerRemoteDatabaseEl = containerEl.createDiv(); const containerRemoteDatabaseEl = containerEl.createDiv();
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" }); containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while automatic synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` }); const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while automatic synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` });
@@ -591,6 +620,31 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}); });
text.inputEl.setAttribute("type", "number"); text.inputEl.setAttribute("type", "number");
}); });
let newDatabaseName = this.plugin.settings.additionalSuffixOfDatabaseName + "";
new Setting(containerLocalDatabaseEl)
.setName("Database suffix")
.setDesc("Set unique name for using same vault name on different directory.")
.addText((text) => {
text.setPlaceholder("")
.setValue(newDatabaseName)
.onChange((value) => {
newDatabaseName = value;
});
}).addButton((button) => {
button.setButtonText("Change")
.onClick(async () => {
if (this.plugin.settings.additionalSuffixOfDatabaseName == newDatabaseName) {
Logger("Suffix was not changed.", LOG_LEVEL.NOTICE);
return;
}
this.plugin.settings.additionalSuffixOfDatabaseName = newDatabaseName;
await this.plugin.saveSettings();
Logger("Suffix has been changed. Reopening database...", LOG_LEVEL.NOTICE);
await this.plugin.initializeDatabase();
})
})
addScreenElement("10", containerLocalDatabaseEl); addScreenElement("10", containerLocalDatabaseEl);
const containerGeneralSettingsEl = containerEl.createDiv(); const containerGeneralSettingsEl = containerEl.createDiv();
@@ -614,6 +668,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerGeneralSettingsEl)
.setName("Delete metadata of deleted files.")
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.deleteMetadataOfDeletedFiles).onChange(async (value) => {
this.plugin.settings.deleteMetadataOfDeletedFiles = value;
await this.plugin.saveSettings();
})
}
);
addScreenElement("20", containerGeneralSettingsEl); addScreenElement("20", containerGeneralSettingsEl);
const containerSyncSettingEl = containerEl.createDiv(); const containerSyncSettingEl = containerEl.createDiv();
@@ -787,6 +850,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
); );
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Scan hidden files periodicaly.") .setName("Scan hidden files periodicaly.")
.setDesc("Seconds, zero to disable.")
.addText((text) => { .addText((text) => {
text.setPlaceholder("") text.setPlaceholder("")
.setValue(this.plugin.settings.syncInternalFilesInterval + "") .setValue(this.plugin.settings.syncInternalFilesInterval + "")
@@ -800,12 +864,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}); });
text.inputEl.setAttribute("type", "number"); text.inputEl.setAttribute("type", "number");
}); });
let skipPatternTextArea: TextAreaComponent = null;
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$";
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Skip patterns") .setName("Skip patterns")
.setDesc( .setDesc(
"Regular expression" "Regular expression, If you use hidden file sync between desktop and mobile, adding `workspace$` is recommended."
) )
.addTextArea((text) => .addTextArea((text) => {
text text
.setValue(this.plugin.settings.syncInternalFilesIgnorePatterns) .setValue(this.plugin.settings.syncInternalFilesIgnorePatterns)
.setPlaceholder("\\/node_modules\\/, \\/\\.git\\/") .setPlaceholder("\\/node_modules\\/, \\/\\.git\\/")
@@ -813,7 +880,52 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.syncInternalFilesIgnorePatterns = value; this.plugin.settings.syncInternalFilesIgnorePatterns = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
skipPatternTextArea = text;
return text;
}
); );
new Setting(containerSyncSettingEl)
.setName("Skip patterns defaults")
.addButton((button) => {
button.setButtonText("Default")
.onClick(async () => {
skipPatternTextArea.setValue(defaultSkipPattern);
this.plugin.settings.syncInternalFilesIgnorePatterns = defaultSkipPattern;
await this.plugin.saveSettings();
})
}).addButton((button) => {
button.setButtonText("Cross-platform")
.onClick(async () => {
skipPatternTextArea.setValue(defaultSkipPatternXPlat);
this.plugin.settings.syncInternalFilesIgnorePatterns = defaultSkipPatternXPlat;
await this.plugin.saveSettings();
})
})
new Setting(containerSyncSettingEl)
.setName("Touch hidden files")
.setDesc("Update the modified time of all hidden files to the current time.")
.addButton((button) =>
button
.setButtonText("Touch")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-left")
.onClick(async () => {
const filesAll = await this.plugin.scanInternalFiles();
const targetFiles = await this.plugin.filterTargetFiles(filesAll);
const now = Date.now();
const newFiles = targetFiles.map(e => ({ ...e, mtime: now }));
let i = 0;
const maxFiles = newFiles.length;
for (const file of newFiles) {
i++;
Logger(`Touched:${file.path} (${i}/${maxFiles})`, LOG_LEVEL.NOTICE, "touch-files");
await this.plugin.applyMTimeToFile(file);
}
})
)
containerSyncSettingEl.createEl("h3", { containerSyncSettingEl.createEl("h3", {
text: sanitizeHTMLToDom(`Advanced settings`), text: sanitizeHTMLToDom(`Advanced settings`),
}); });
@@ -1221,6 +1333,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
} }
applyDisplayEnabled(); applyDisplayEnabled();
addScreenElement("70", containerCorruptedDataEl); addScreenElement("70", containerCorruptedDataEl);
changeDisplay("0"); if (lastVersion != this.plugin.settings.lastReadUpdates) {
changeDisplay("100");
} else {
changeDisplay("0");
}
} }
} }

126
src/dialogs.ts Normal file
View File

@@ -0,0 +1,126 @@
import { App, FuzzySuggestModal, Modal, Setting } from "obsidian";
import ObsidianLiveSyncPlugin from "./main";
//@ts-ignore
import PluginPane from "./PluginPane.svelte";
export class PluginDialogModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
logEl: HTMLDivElement;
component: PluginPane = null;
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
super(app);
this.plugin = plugin;
}
onOpen() {
const { contentEl } = this;
if (this.component == null) {
this.component = new PluginPane({
target: contentEl,
props: { plugin: this.plugin },
});
}
}
onClose() {
if (this.component != null) {
this.component.$destroy();
this.component = null;
}
}
}
export class InputStringDialog extends Modal {
result: string | false = false;
onSubmit: (result: string | boolean) => void;
title: string;
key: string;
placeholder: string;
isManuallyClosed = false;
constructor(app: App, title: string, key: string, placeholder: string, onSubmit: (result: string | false) => void) {
super(app);
this.onSubmit = onSubmit;
this.title = title;
this.placeholder = placeholder;
this.key = key;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h1", { text: this.title });
new Setting(contentEl).setName(this.key).addText((text) =>
text.onChange((value) => {
this.result = value;
})
);
new Setting(contentEl).addButton((btn) =>
btn
.setButtonText("Ok")
.setCta()
.onClick(() => {
this.isManuallyClosed = true;
this.close();
})
).addButton((btn) =>
btn
.setButtonText("Cancel")
.setCta()
.onClick(() => {
this.close();
})
);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
if (this.isManuallyClosed) {
this.onSubmit(this.result);
} else {
this.onSubmit(false);
}
}
}
export class PopoverSelectString extends FuzzySuggestModal<string> {
app: App;
callback: (e: string) => void = () => { };
getItemsFun: () => string[] = () => {
return ["yes", "no"];
}
constructor(app: App, note: string, placeholder: string | null, getItemsFun: () => string[], callback: (e: string) => void) {
super(app);
this.app = app;
this.setPlaceholder(placeholder ?? "y/n) " + note);
if (getItemsFun) this.getItemsFun = getItemsFun;
this.callback = callback;
}
getItems(): string[] {
return this.getItemsFun();
}
getItemText(item: string): string {
return item;
}
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
// debugger;
this.callback(item);
this.callback = null;
}
onClose(): void {
setTimeout(() => {
if (this.callback != null) {
this.callback("");
}
}, 100);
}
}

Submodule src/lib updated: 1f67fb604c...a49a096a6a

View File

@@ -1,4 +1,4 @@
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal, Setting } from "obsidian"; import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App, } from "obsidian";
import { diff_match_patch } from "diff-match-patch"; import { diff_match_patch } from "diff-match-patch";
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry } from "./lib/src/types"; import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry } from "./lib/src/types";
@@ -27,137 +27,67 @@ import { ConflictResolveModal } from "./ConflictResolveModal";
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab"; import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
import { DocumentHistoryModal } from "./DocumentHistoryModal"; import { DocumentHistoryModal } from "./DocumentHistoryModal";
//@ts-ignore
import PluginPane from "./PluginPane.svelte";
import { id2path, path2id } from "./utils"; import { clearAllPeriodic, clearAllTriggers, disposeMemoObject, id2path, memoIfNotExist, memoObject, path2id, retriveMemoObject, setTrigger } from "./utils";
import { decrypt, encrypt } from "./lib/src/e2ee_v2"; import { decrypt, encrypt } from "./lib/src/e2ee_v2";
const isDebug = false; const isDebug = false;
import { InputStringDialog, PluginDialogModal, PopoverSelectString } from "./dialogs";
setNoticeClass(Notice); setNoticeClass(Notice);
class PluginDialogModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
logEl: HTMLDivElement;
component: PluginPane = null;
constructor(app: App, plugin: ObsidianLiveSyncPlugin) { const ICHeader = "i:";
super(app); const ICHeaderEnd = "i;";
this.plugin = plugin; const ICHeaderLength = ICHeader.length;
}
onOpen() {
const { contentEl } = this;
if (this.component == null) {
this.component = new PluginPane({
target: contentEl,
props: { plugin: this.plugin },
});
}
}
onClose() { /**
if (this.component != null) { * returns is internal chunk of file
this.component.$destroy(); * @param str ID
this.component = null; * @returns
} */
} function isInteralChunk(str: string): boolean {
return str.startsWith(ICHeader);
}
function id2filenameInternalChunk(str: string): string {
return str.substring(ICHeaderLength);
}
function filename2idInternalChunk(str: string): string {
return ICHeader + str;
} }
class InputStringDialog extends Modal { const CHeader = "h:";
result: string | false = false; const CHeaderEnd = "h;";
onSubmit: (result: string | boolean) => void; // const CHeaderLength = CHeader.length;
title: string; function isChunk(str: string): boolean {
key: string; return str.startsWith(CHeader);
placeholder: string;
isManuallyClosed = false;
constructor(app: App, title: string, key: string, placeholder: string, onSubmit: (result: string | false) => void) {
super(app);
this.onSubmit = onSubmit;
this.title = title;
this.placeholder = placeholder;
this.key = key;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h1", { text: this.title });
new Setting(contentEl).setName(this.key).addText((text) =>
text.onChange((value) => {
this.result = value;
})
);
new Setting(contentEl).addButton((btn) =>
btn
.setButtonText("Ok")
.setCta()
.onClick(() => {
this.isManuallyClosed = true;
this.close();
})
).addButton((btn) =>
btn
.setButtonText("Cancel")
.setCta()
.onClick(() => {
this.close();
})
);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
if (this.isManuallyClosed) {
this.onSubmit(this.result);
} else {
this.onSubmit(false);
}
}
} }
class PopoverYesNo extends FuzzySuggestModal<string> {
app: App;
callback: (e: string) => void = () => { };
constructor(app: App, note: string, callback: (e: string) => void) { const PSCHeader = "ps:";
super(app); const PSCHeaderEnd = "ps;";
this.app = app; function isPluginChunk(str: string): boolean {
this.setPlaceholder("y/n) " + note); return str.startsWith(PSCHeader);
this.callback = callback;
}
getItems(): string[] {
return ["yes", "no"];
}
getItemText(item: string): string {
return item;
}
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
// debugger;
this.callback(item);
this.callback = null;
}
onClose(): void {
setTimeout(() => {
if (this.callback != null) {
this.callback("");
}
}, 100);
}
} }
const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => { const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
return new Promise((res) => { return new Promise((res) => {
const popover = new PopoverYesNo(app, message, (result) => res(result as "yes" | "no")); const popover = new PopoverSelectString(app, message, null, null, (result) => res(result as "yes" | "no"));
popover.open(); popover.open();
}); });
}; };
const askSelectString = (app: App, message: string, items: string[]): Promise<string> => {
const getItemsFun = () => items;
return new Promise((res) => {
const popover = new PopoverSelectString(app, message, "Select file)", getItemsFun, (result) => res(result));
popover.open();
});
};
const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => { const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => {
return new Promise((res) => { return new Promise((res) => {
const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result)); const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result));
@@ -189,6 +119,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
deviceAndVaultName: string; deviceAndVaultName: string;
isMobile = false; isMobile = false;
getVaultName(): string {
return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
}
setInterval(handler: () => any, timeout?: number): number { setInterval(handler: () => any, timeout?: number): number {
const timer = window.setInterval(handler, timeout); const timer = window.setInterval(handler, timeout);
this.registerInterval(timer); this.registerInterval(timer);
@@ -203,7 +137,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return false; return false;
} }
showHistory(file: TFile) { showHistory(file: TFile | string) {
if (!this.settings.useHistory) { if (!this.settings.useHistory) {
Logger("You have to enable Use History in misc.", LOG_LEVEL.NOTICE); Logger("You have to enable Use History in misc.", LOG_LEVEL.NOTICE);
} else { } else {
@@ -211,12 +145,53 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
async fileHistory() {
const pageLimit = 1000;
let nextKey = "";
const notes: { path: string, mtime: number }[] = [];
do {
const docs = await this.localDatabase.localDatabase.allDocs({ limit: pageLimit, startkey: nextKey, include_docs: true });
nextKey = "";
for (const row of docs.rows) {
const doc = row.doc;
nextKey = `${row.id}\u{10ffff}`;
if (!("type" in doc)) continue;
if (doc.type == "newnote" || doc.type == "plain") {
// const docId = doc._id.startsWith("i:") ? doc._id.substring("i:".length) : doc._id;
notes.push({ path: id2path(doc._id), mtime: doc.mtime });
}
if (isChunk(nextKey)) {
// skip the chunk zone.
nextKey = CHeaderEnd;
}
}
} while (nextKey != "");
notes.sort((a, b) => b.mtime - a.mtime);
const notesList = notes.map(e => e.path);
const target = await askSelectString(this.app, "File to view History", notesList);
if (target) {
this.showHistory(target);
}
}
async onload() { async onload() {
setLogger(this.addLog.bind(this)); // Logger moved to global. setLogger(this.addLog.bind(this)); // Logger moved to global.
Logger("loading plugin"); Logger("loading plugin");
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName(); //@ts-ignore
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
//@ts-ignore
const packageVersion: string = PACKAGE_VERSION || "0.0.0";
Logger(`Self-hosted LiveSync v${manifestVersion} ${packageVersion} `);
const lsname = "obsidian-live-sync-ver" + this.getVaultName();
const last_version = localStorage.getItem(lsname); const last_version = localStorage.getItem(lsname);
await this.loadSettings(); await this.loadSettings();
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
if (lastVersion > this.settings.lastReadUpdates) {
Logger("Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.", LOG_LEVEL.NOTICE);
}
//@ts-ignore //@ts-ignore
if (this.app.isMobile) { if (this.app.isMobile) {
this.isMobile = true; this.isMobile = true;
@@ -228,7 +203,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.settings.syncOnStart = false; this.settings.syncOnStart = false;
this.settings.syncOnFileOpen = false; this.settings.syncOnFileOpen = false;
this.settings.periodicReplication = false; this.settings.periodicReplication = false;
this.settings.versionUpFlash = "I changed specifications incompatiblly, so when you enable sync again, be sure to made version up all nother devides."; this.settings.versionUpFlash = "Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.";
this.saveSettings(); this.saveSettings();
} }
localStorage.setItem(lsname, `${VER}`); localStorage.setItem(lsname, `${VER}`);
@@ -296,6 +271,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.settings.autoSweepPlugins = false; this.settings.autoSweepPlugins = false;
this.settings.usePluginSync = false; this.settings.usePluginSync = false;
this.settings.suspendFileWatching = true; this.settings.suspendFileWatching = true;
this.settings.syncInternalFiles = false;
await this.saveSettings(); await this.saveSettings();
await this.openDatabase(); await this.openDatabase();
const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured."; const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured.";
@@ -489,6 +465,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.showHistory(view.file); this.showHistory(view.file);
}, },
}); });
this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000); this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000);
this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000); this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000);
setLockNotifier(() => { setLockNotifier(() => {
@@ -509,6 +486,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.syncInternalFilesAndDatabase("safe", true); this.syncInternalFilesAndDatabase("safe", true);
}, },
}); });
this.addCommand({
id: "livesync-filehistory",
name: "Pick a file to show history",
callback: () => {
this.fileHistory();
},
})
} }
pluginDialog: PluginDialogModal = null; pluginDialog: PluginDialogModal = null;
@@ -545,6 +530,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.localDatabase.closeReplication(); this.localDatabase.closeReplication();
this.localDatabase.close(); this.localDatabase.close();
} }
clearAllPeriodic();
clearAllTriggers();
window.removeEventListener("visibilitychange", this.watchWindowVisiblity); window.removeEventListener("visibilitychange", this.watchWindowVisiblity);
Logger("unloading plugin"); Logger("unloading plugin");
} }
@@ -553,7 +540,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.localDatabase != null) { if (this.localDatabase != null) {
this.localDatabase.close(); this.localDatabase.close();
} }
const vaultName = this.app.vault.getName(); const vaultName = this.getVaultName();
Logger("Open Database..."); Logger("Open Database...");
//@ts-ignore //@ts-ignore
const isMobile = this.app.isMobile; const isMobile = this.app.isMobile;
@@ -580,7 +567,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
// So, use history is always enabled. // So, use history is always enabled.
this.settings.useHistory = true; this.settings.useHistory = true;
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.app.vault.getName(); const lsname = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
if (this.settings.deviceAndVaultName != "") { if (this.settings.deviceAndVaultName != "") {
if (!localStorage.getItem(lsname)) { if (!localStorage.getItem(lsname)) {
this.deviceAndVaultName = this.settings.deviceAndVaultName; this.deviceAndVaultName = this.settings.deviceAndVaultName;
@@ -596,7 +583,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async saveSettings() { async saveSettings() {
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.app.vault.getName(); const lsname = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
localStorage.setItem(lsname, this.deviceAndVaultName || ""); localStorage.setItem(lsname, this.deviceAndVaultName || "");
await this.saveData(this.settings); await this.saveData(this.settings);
@@ -834,7 +821,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
try { try {
Logger(`file save ${file.path} into db`); Logger(`file save ${file.path} into db`);
await this.updateIntoDB(file); await this.updateIntoDB(file);
Logger(`deleted ${oldFile} into db`); Logger(`deleted ${oldFile} from db`);
await this.deleteFromDBbyPath(oldFile); await this.deleteFromDBbyPath(oldFile);
} catch (ex) { } catch (ex) {
Logger(ex); Logger(ex);
@@ -859,7 +846,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL.VERBOSE) { if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL.VERBOSE) {
return; return;
} }
const valutName = this.app.vault.getName(); const valutName = this.getVaultName();
const timestamp = new Date().toLocaleString(); const timestamp = new Date().toLocaleString();
const messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); const messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
const newmessage = timestamp + "->" + messagecontent; const newmessage = timestamp + "->" + messagecontent;
@@ -1009,7 +996,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (shouldBeIgnored(pathSrc)) { if (shouldBeIgnored(pathSrc)) {
return; return;
} }
if (docEntry._deleted) { if (docEntry._deleted || docEntry.deleted) {
//basically pass. //basically pass.
//but if there are no docs left, delete file. //but if there are no docs left, delete file.
const lastDocs = await this.localDatabase.getDBEntry(pathSrc); const lastDocs = await this.localDatabase.getDBEntry(pathSrc);
@@ -1080,7 +1067,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async handleDBChanged(change: EntryBody) { async handleDBChanged(change: EntryBody) {
const targetFile = this.app.vault.getAbstractFileByPath(id2path(change._id)); const targetFile = this.app.vault.getAbstractFileByPath(id2path(change._id));
if (targetFile == null) { if (targetFile == null) {
if (change._deleted) { if (change._deleted || change.deleted) {
return; return;
} }
const doc = change; const doc = change;
@@ -1113,11 +1100,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
saveQueuedFiles() { saveQueuedFiles() {
const saveData = JSON.stringify(this.queuedFiles.filter((e) => !e.done).map((e) => e.entry._id)); const saveData = JSON.stringify(this.queuedFiles.filter((e) => !e.done).map((e) => e.entry._id));
const lsname = "obsidian-livesync-queuefiles-" + this.app.vault.getName(); const lsname = "obsidian-livesync-queuefiles-" + this.getVaultName();
localStorage.setItem(lsname, saveData); localStorage.setItem(lsname, saveData);
} }
async loadQueuedFiles() { async loadQueuedFiles() {
const lsname = "obsidian-livesync-queuefiles-" + this.app.vault.getName(); const lsname = "obsidian-livesync-queuefiles-" + this.getVaultName();
const ids = JSON.parse(localStorage.getItem(lsname) || "[]") as string[]; const ids = JSON.parse(localStorage.getItem(lsname) || "[]") as string[];
const ret = await this.localDatabase.localDatabase.allDocs({ keys: ids, include_docs: true }); const ret = await this.localDatabase.localDatabase.allDocs({ keys: ids, include_docs: true });
for (const doc of ret.rows) { for (const doc of ret.rows) {
@@ -1134,9 +1121,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const now = new Date().getTime(); const now = new Date().getTime();
if (queue.missingChildren.length == 0) { if (queue.missingChildren.length == 0) {
queue.done = true; queue.done = true;
if (queue.entry._id.startsWith("i:")) { if (isInteralChunk(queue.entry._id)) {
//system file //system file
const filename = id2path(queue.entry._id.substring("i:".length)); const filename = id2path(id2filenameInternalChunk(queue.entry._id));
Logger(`Applying hidden file, ${queue.entry._id} (${queue.entry._rev}) change...`); Logger(`Applying hidden file, ${queue.entry._id} (${queue.entry._rev}) change...`);
await this.syncInternalFilesAndDatabase("pull", false, false, [filename]) await this.syncInternalFilesAndDatabase("pull", false, false, [filename])
Logger(`Applied hidden file, ${queue.entry._id} (${queue.entry._rev}) change...`); Logger(`Applied hidden file, ${queue.entry._id} (${queue.entry._rev}) change...`);
@@ -1179,7 +1166,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) { async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary. const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
if ((!doc._id.startsWith("i:")) && skipOldFile) { if ((!isInteralChunk(doc._id)) && skipOldFile) {
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id)); const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
if (info && info instanceof TFile) { if (info && info instanceof TFile) {
@@ -1216,13 +1203,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> { async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
this.refreshStatusText(); this.refreshStatusText();
for (const change of docs) { for (const change of docs) {
if (change._id.startsWith("ps:")) { if (isPluginChunk(change._id)) {
if (this.settings.notifyPluginOrSettingUpdated) { if (this.settings.notifyPluginOrSettingUpdated) {
this.triggerCheckPluginUpdate(); this.triggerCheckPluginUpdate();
} }
continue; continue;
} }
if (change._id.startsWith("h:")) { if (isChunk(change._id)) {
await this.parseIncomingChunk(change); await this.parseIncomingChunk(change);
continue; continue;
} }
@@ -1482,7 +1469,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const filesStorage = this.app.vault.getFiles(); const filesStorage = this.app.vault.getFiles();
const filesStorageName = filesStorage.map((e) => e.path); const filesStorageName = filesStorage.map((e) => e.path);
const wf = await this.localDatabase.localDatabase.allDocs(); const wf = await this.localDatabase.localDatabase.allDocs();
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && !e.id.startsWith("ps:") && e.id != "obsydian_livesync_version").filter(e => isValidPath(e.id)).map((e) => id2path(e.id)); const filesDatabase = wf.rows.filter((e) => !isChunk(e.id) && !isPluginChunk(e.id) && e.id != "obsydian_livesync_version").filter(e => isValidPath(e.id)).map((e) => id2path(e.id));
const isInitialized = await (this.localDatabase.kvDB.get<boolean>("initialized")) || false; const isInitialized = await (this.localDatabase.kvDB.get<boolean>("initialized")) || false;
// Make chunk bigger if it is the initial scan. There must be non-active docs. // Make chunk bigger if it is the initial scan. There must be non-active docs.
if (filesDatabase.length == 0 && !isInitialized) { if (filesDatabase.length == 0 && !isInitialized) {
@@ -1499,7 +1486,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger("Updating database by new files"); Logger("Updating database by new files");
this.setStatusBarText(`UPDATE DATABASE`); this.setStatusBarText(`UPDATE DATABASE`);
const runAll = async <T>(procedurename: string, objects: T[], callback: (arg: T) => Promise<void>) => { const runAll = async<T>(procedurename: string, objects: T[], callback: (arg: T) => Promise<void>) => {
const count = objects.length; const count = objects.length;
Logger(procedurename); Logger(procedurename);
let i = 0; let i = 0;
@@ -1535,6 +1522,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await p.all(); await p.all();
Logger(`${procedurename} done.`); Logger(`${procedurename} done.`);
}; };
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => { await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
Logger(`Update into ${e.path}`); Logger(`Update into ${e.path}`);
@@ -1542,7 +1530,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}); });
if (!initialScan) { if (!initialScan) {
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => { await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
Logger(`Pull from db:${e}`); Logger(`Check or pull from db:${e}`);
await this.pullFile(e, filesStorage, false, null, false); await this.pullFile(e, filesStorage, false, null, false);
}); });
} }
@@ -1563,7 +1551,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
this.setStatusBarText(`NOW TRACKING!`); this.setStatusBarText(`NOW TRACKING!`);
Logger("Initialized,NOW TRACKING!"); Logger("Initialized, NOW TRACKING!");
if (!isInitialized) { if (!isInitialized) {
await (this.localDatabase.kvDB.set("initialized", true)) await (this.localDatabase.kvDB.set("initialized", true))
} }
@@ -1816,13 +1804,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (targetFile == null) { if (targetFile == null) {
//have to create; //have to create;
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady); const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
if (doc === false) return; if (doc === false) {
Logger(`${filename} Skipped`);
return;
}
await this.doc2storage_create(doc, force); await this.doc2storage_create(doc, force);
} else if (targetFile instanceof TFile) { } else if (targetFile instanceof TFile) {
//normal case //normal case
const file = targetFile; const file = targetFile;
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady); const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
if (doc === false) return; if (doc === false) {
Logger(`${filename} Skipped`);
return;
}
await this.doc2storage_modify(doc, file, force); await this.doc2storage_modify(doc, file, force);
} else { } else {
Logger(`target files:${filename} is exists as the folder`); Logger(`target files:${filename} is exists as the folder`);
@@ -1866,6 +1860,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false); const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
if (docx != false) { if (docx != false) {
await this.doc2storage_modify(docx, file); await this.doc2storage_modify(docx, file);
} else {
Logger("STORAGE <- DB :" + file.path + " Skipped");
} }
caches[dK] = { storageMtime, docMtime }; caches[dK] = { storageMtime, docMtime };
return caches; return caches;
@@ -1912,10 +1908,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
const old = await this.localDatabase.getDBEntry(fullpath, null, false, false); const old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
if (old !== false) { if (old !== false) {
const oldData = { data: old.data, deleted: old._deleted }; const oldData = { data: old.data, deleted: old._deleted || old.deleted, };
const newData = { data: d.data, deleted: d._deleted }; const newData = { data: d.data, deleted: d._deleted || d.deleted };
if (JSON.stringify(oldData) == JSON.stringify(newData)) { if (JSON.stringify(oldData) == JSON.stringify(newData)) {
Logger(msg + "Skipped (not changed) " + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE); Logger(msg + "Skipped (not changed) " + fullpath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
return true; return true;
} }
// d._rev = old._rev; // d._rev = old._rev;
@@ -1968,7 +1964,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async getPluginList(): Promise<{ plugins: PluginList; allPlugins: DevicePluginList; thisDevicePlugins: DevicePluginList }> { async getPluginList(): Promise<{ plugins: PluginList; allPlugins: DevicePluginList; thisDevicePlugins: DevicePluginList }> {
const db = this.localDatabase.localDatabase; const db = this.localDatabase.localDatabase;
const docList = await db.allDocs<PluginDataEntry>({ startkey: `ps:`, endkey: `ps;`, include_docs: false }); const docList = await db.allDocs<PluginDataEntry>({ startkey: PSCHeader, endkey: PSCHeaderEnd, include_docs: false });
const oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.localDatabase.getDBEntry(e.id)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(e.data)); const oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.localDatabase.getDBEntry(e.id)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(e.data));
const plugins: { [key: string]: PluginDataEntry[] } = {}; const plugins: { [key: string]: PluginDataEntry[] } = {};
const allPlugins: { [key: string]: PluginDataEntry } = {}; const allPlugins: { [key: string]: PluginDataEntry } = {};
@@ -2064,7 +2060,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
Logger(`Deleting old plugins`, LOG_LEVEL.VERBOSE); Logger(`Deleting old plugins`, LOG_LEVEL.VERBOSE);
const delDocs = oldDocs.rows.map((e) => { const delDocs = oldDocs.rows.map((e) => {
e.doc._deleted = true; // e.doc._deleted = true;
if (e.doc.type == "newnote" || e.doc.type == "plain") {
e.doc.deleted = true;
if (this.settings.deleteMetadataOfDeletedFiles) {
e.doc._deleted = true;
}
} else {
e.doc._deleted = true;
}
return e.doc; return e.doc;
}); });
await db.bulkDocs(delDocs); await db.bulkDocs(delDocs);
@@ -2145,30 +2149,39 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async getFiles( async getFiles(
path: string, path: string,
ignoreList: string[], ignoreList: string[],
filter: RegExp[] filter: RegExp[],
ignoreFilter: RegExp[],
) { ) {
const w = await this.app.vault.adapter.list(path); const w = await this.app.vault.adapter.list(path);
let files = [ let files = [
...w.files ...w.files
.filter((e) => !ignoreList.some((ee) => e.endsWith(ee))) .filter((e) => !ignoreList.some((ee) => e.endsWith(ee)))
.filter((e) => !filter || filter.some((ee) => e.match(ee))), .filter((e) => !filter || filter.some((ee) => e.match(ee)))
.filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))),
]; ];
L1: for (const v of w.folders) { L1: for (const v of w.folders) {
for (const ignore of ignoreList) { for (const ignore of ignoreList) {
if (v.endsWith(ignore)) { if (v.endsWith(ignore)) {
continue L1; continue L1;
} }
} }
files = files.concat(await this.getFiles(v, ignoreList, filter)); if (ignoreFilter && ignoreFilter.some(e => v.match(e))) {
continue L1;
}
files = files.concat(await this.getFiles(v, ignoreList, filter, ignoreFilter));
} }
return files; return files;
} }
async scanInternalFiles(): Promise<InternalFileInfo[]> { async scanInternalFiles(): Promise<InternalFileInfo[]> {
const ignoreFiles = ["node_modules", ".git", "obsidian-pouch"]; const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns.toLocaleLowerCase()
.replace(/\n| /g, "")
.split(",").filter(e => e).map(e => new RegExp(e));
const root = this.app.vault.getRoot(); const root = this.app.vault.getRoot();
const findRoot = root.path; const findRoot = root.path;
const filenames = (await this.getFiles(findRoot, ignoreFiles, null)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); const filenames = (await this.getFiles(findRoot, [], null, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash"));
const files = filenames.map(async e => { const files = filenames.map(async e => {
return { return {
path: e, path: e,
@@ -2187,7 +2200,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async storeInternaFileToDatabase(file: InternalFileInfo, forceWrite = false) { async storeInternaFileToDatabase(file: InternalFileInfo, forceWrite = false) {
const id = "i:" + path2id(file.path); const id = filename2idInternalChunk(path2id(file.path));
const contentBin = await this.app.vault.adapter.readBinary(file.path); const contentBin = await this.app.vault.adapter.readBinary(file.path);
const content = await arrayBufferToBase64(contentBin); const content = await arrayBufferToBase64(contentBin);
const mtime = file.mtime; const mtime = file.mtime;
@@ -2224,12 +2237,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
await this.localDatabase.putDBEntry(saveData, true); await this.localDatabase.putDBEntry(saveData, true);
Logger(`internal files STORAGE --> DB:${file.path}: Done`); Logger(`STORAGE --> DB:${file.path}: (hidden) Done`);
}); });
} }
async deleteInternaFileOnDatabase(filename: string, forceWrite = false) { async deleteInternaFileOnDatabase(filename: string, forceWrite = false) {
const id = "i:" + path2id(filename); const id = filename2idInternalChunk(path2id(filename));
const mtime = new Date().getTime(); const mtime = new Date().getTime();
await runWithLock("file-" + id, false, async () => { await runWithLock("file-" + id, false, async () => {
const old = await this.localDatabase.getDBEntry(id, null, false, false) as InternalFileEntry | false; const old = await this.localDatabase.getDBEntry(id, null, false, false) as InternalFileEntry | false;
@@ -2286,7 +2299,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async extractInternaFileFromDatabase(filename: string, force = false) { async extractInternaFileFromDatabase(filename: string, force = false) {
const isExists = await this.app.vault.adapter.exists(filename); const isExists = await this.app.vault.adapter.exists(filename);
const id = "i:" + path2id(filename); const id = filename2idInternalChunk(path2id(filename));
return await runWithLock("file-" + id, false, async () => { return await runWithLock("file-" + id, false, async () => {
const fileOnDB = await this.localDatabase.getDBEntry(id, null, false, false) as false | LoadedEntry; const fileOnDB = await this.localDatabase.getDBEntry(id, null, false, false) as false | LoadedEntry;
@@ -2314,7 +2327,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const contentBin = await this.app.vault.adapter.readBinary(filename); const contentBin = await this.app.vault.adapter.readBinary(filename);
const content = await arrayBufferToBase64(contentBin); const content = await arrayBufferToBase64(contentBin);
if (content == fileOnDB.data && !force) { if (content == fileOnDB.data && !force) {
Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`); // Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`, LOG_LEVEL.VERBOSE);
return false; return false;
} }
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime }); await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
@@ -2328,8 +2341,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}); });
} }
filterTargetFiles(files: InternalFileInfo[], targetFiles: string[] | false = false) {
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns.toLocaleLowerCase()
.replace(/\n| /g, "")
.split(",").filter(e => e).map(e => new RegExp(e));
return files.filter(file => !ignorePatterns.some(e => file.path.match(e))).filter(file => !targetFiles || (targetFiles && targetFiles.indexOf(file.path) !== -1))
}
async applyMTimeToFile(file: InternalFileInfo) {
await this.app.vault.adapter.append(file.path, "", { ctime: file.ctime, mtime: file.mtime });
}
confirmPopup: WrappedNotice = null; confirmPopup: WrappedNotice = null;
confirmPopupTimer: number = null;
//TODO: Tidy up. Even though it is experimental feature, So dirty...
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) { async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO; const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
Logger("Scanning hidden files.", logLevel, "sync_internal"); Logger("Scanning hidden files.", logLevel, "sync_internal");
@@ -2337,8 +2361,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
.replace(/\n| /g, "") .replace(/\n| /g, "")
.split(",").filter(e => e).map(e => new RegExp(e)); .split(",").filter(e => e).map(e => new RegExp(e));
if (!files) files = await this.scanInternalFiles(); if (!files) files = await this.scanInternalFiles();
const filesOnDB = (await this.localDatabase.localDatabase.allDocs({ startkey: "i:", endkey: "i;", include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]; const filesOnDB = ((await this.localDatabase.localDatabase.allDocs({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
const allFileNames = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => normalizePath(id2path(e._id.substring("i:".length))))])];
const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => normalizePath(id2path(id2filenameInternalChunk(e._id))))])];
const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1))
function compareMTime(a: number, b: number) { function compareMTime(a: number, b: number) {
const wa = ~~(a / 1000); const wa = ~~(a / 1000);
const wb = ~~(b / 1000); const wb = ~~(b / 1000);
@@ -2351,35 +2377,68 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
let filesChanged = 0; let filesChanged = 0;
const p = Parallels(); const p = Parallels();
const limit = 10; const limit = 10;
// count updated files up as like this below:
// .obsidian: 2
// .obsidian/workspace: 1
// .obsidian/plugins: 1
// .obsidian/plugins/recent-files-obsidian: 1
// .obsidian/plugins/recent-files-obsidian/data.json: 1
const updatedFolders: { [key: string]: number } = {}
const countUpdatedFolder = (path: string) => {
const pieces = path.split("/");
let c = pieces.shift();
let pathPieces = "";
filesChanged++;
while (c) {
pathPieces += (pathPieces != "" ? "/" : "") + c;
pathPieces = normalizePath(pathPieces);
if (!(pathPieces in updatedFolders)) {
updatedFolders[pathPieces] = 0;
}
updatedFolders[pathPieces]++;
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.localDatabase.kvDB.get<{ [key: string]: { storageMtime: number; docMtime: number } }>("diff-caches-internal") || {};
for (const filename of allFileNames) { for (const filename of allFileNames) {
// Logger(`Processing:${filename}`, LOG_LEVEL.VERBOSE);
processed++; processed++;
if (processed % 100 == 0) Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal"); if (processed % 100 == 0) Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal");
if (ignorePatterns.some(e => filename.match(e))) continue; if (ignorePatterns.some(e => filename.match(e))) continue;
if (targetFiles !== false && targetFiles.indexOf(filename) == -1) continue;
const fileOnStorage = files.find(e => e.path == filename); const fileOnStorage = files.find(e => e.path == filename);
const fileOnDatabase = filesOnDB.find(e => e._id == "i:" + id2path(filename)); const fileOnDatabase = filesOnDB.find(e => e._id == filename2idInternalChunk(id2path(filename)));
let proc: () => Promise<void> | null = null; // TODO: Fix this somehow smart.
let proc: Promise<void> | null;
if (fileOnStorage && fileOnDatabase) { if (fileOnStorage && fileOnDatabase) {
// Both => Synchronize // Both => Synchronize
const cache = filename in caches ? caches[filename] : { storageMtime: 0, docMtime: 0 };
if (fileOnDatabase.mtime == cache.docMtime && fileOnStorage.mtime == cache.storageMtime) {
continue;
}
const nw = compareMTime(fileOnStorage.mtime, fileOnDatabase.mtime); const nw = compareMTime(fileOnStorage.mtime, fileOnDatabase.mtime);
if (nw == 0) continue; if (nw == 0) continue;
if (nw > 0) { if (nw > 0) {
proc = async () => { proc = (async (fileOnStorage) => {
await this.storeInternaFileToDatabase(fileOnStorage); await this.storeInternaFileToDatabase(fileOnStorage);
} cache.docMtime = fileOnDatabase.mtime;
cache.storageMtime = fileOnStorage.mtime;
caches[filename] = cache;
})(fileOnStorage);
} }
if (nw < 0) { if (nw < 0) {
proc = async () => { proc = (async (filename) => {
if (await this.extractInternaFileFromDatabase(filename)) { if (await this.extractInternaFileFromDatabase(filename)) {
filesChanged++; cache.docMtime = fileOnDatabase.mtime;
cache.storageMtime = fileOnStorage.mtime;
caches[filename] = cache;
countUpdatedFolder(filename);
} }
} })(filename);
} }
} else if (!fileOnStorage && fileOnDatabase) { } else if (!fileOnStorage && fileOnDatabase) {
@@ -2387,71 +2446,137 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (fileOnDatabase.deleted) { if (fileOnDatabase.deleted) {
// await this.storeInternaFileToDatabase(fileOnStorage); // await this.storeInternaFileToDatabase(fileOnStorage);
} else { } else {
proc = async () => { proc = (async () => {
await this.deleteInternaFileOnDatabase(filename); await this.deleteInternaFileOnDatabase(filename);
} })();
} }
} else if (direction == "pull") { } else if (direction == "pull") {
proc = async () => { proc = (async () => {
if (await this.extractInternaFileFromDatabase(filename)) { if (await this.extractInternaFileFromDatabase(filename)) {
filesChanged++; countUpdatedFolder(filename);
} }
} })();
} else if (direction == "safe") { } else if (direction == "safe") {
if (fileOnDatabase.deleted) { if (fileOnDatabase.deleted) {
// await this.storeInternaFileToDatabase(fileOnStorage); // await this.storeInternaFileToDatabase(fileOnStorage);
} else { } else {
proc = async () => { proc = (async () => {
if (await this.extractInternaFileFromDatabase(filename)) { if (await this.extractInternaFileFromDatabase(filename)) {
filesChanged++; countUpdatedFolder(filename);
} }
} })();
} }
} }
} else if (fileOnStorage && !fileOnDatabase) { } else if (fileOnStorage && !fileOnDatabase) {
proc = async () => { proc = (async () => {
await this.storeInternaFileToDatabase(fileOnStorage); await this.storeInternaFileToDatabase(fileOnStorage);
} })();
} else { } else {
throw new Error("Invalid state on hidden file sync"); throw new Error("Invalid state on hidden file sync");
// Something corrupted? // Something corrupted?
} }
if (proc) p.add(proc()); if (proc) p.add(proc);
proc = null;
await p.wait(limit); await p.wait(limit);
} }
await p.all(); await p.all();
// Show notification to restart obsidian. await this.localDatabase.kvDB.set("diff-caches-internal", caches);
// When files has been retreived from the database. they must be reloaded.
if (direction == "pull" && filesChanged != 0) { if (direction == "pull" && filesChanged != 0) {
const configDir = normalizePath(this.app.vault.configDir);
// Show notification to restart obsidian when something has been changed in configDir.
if (configDir in updatedFolders) {
// Numbers of updated files that is below of configDir.
let updatedCount = updatedFolders[configDir];
try {
//@ts-ignore
const manifests = Object.values(this.app.plugins.manifests) as PluginManifest[];
//@ts-ignore
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
const enabledPluginManifests = manifests.filter(e => enabledPlugins.has(e.id));
for (const manifest of enabledPluginManifests) {
if (manifest.dir in updatedFolders) {
// If notified about plug-ins, reloading Obsidian may not be necessary.
updatedCount -= updatedFolders[manifest.dir];
const updatePluginId = manifest.id;
const updatePluginName = manifest.name;
const fragment = createFragment((doc) => {
doc.createEl("span", null, (a) => {
a.appendText(`Files in ${updatePluginName} has been updated, Press `)
a.appendChild(a.createEl("a", null, (anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", async () => {
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL.NOTICE, "pluin-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, "pluin-reload-" + updatePluginId);
});
}))
const fragment = createFragment((doc) => { a.appendText(` to reload ${updatePluginName}, or press elsewhere to dismiss this message.`)
doc.createEl("span", null, (a) => { });
a.appendText(`Hidden files have been synchronized, Press `) });
a.appendChild(a.createEl("a", null, (anchor) => {
anchor.text = "HERE"; const updatedPluginKey = "popupUpdated-" + updatePluginId;
anchor.addEventListener("click", () => { setTrigger(updatedPluginKey, 1000, async () => {
// @ts-ignore const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
this.app.commands.executeCommandById("app:reload") //@ts-ignore
const isShown = popup?.noticeEl?.isShown();
if (!isShown) {
memoObject(updatedPluginKey, new Notice(fragment, 0))
}
setTrigger(updatedPluginKey + "-close", 20000, () => {
const popup = retriveMemoObject<Notice>(updatedPluginKey)
if (!popup) return;
//@ts-ignore
if (popup?.noticeEl?.isShown()) {
popup.hide();
}
disposeMemoObject(updatedPluginKey);
})
})
}
}
} catch (ex) {
Logger("Error on checking plugin status.");
Logger(ex, LOG_LEVEL.VERBOSE);
}
// If something changes left, notify for reloading Obsidian.
if (updatedCount != 0) {
const fragment = createFragment((doc) => {
doc.createEl("span", null, (a) => {
a.appendText(`Hidden files have been synchronized, Press `)
a.appendChild(a.createEl("a", null, (anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
// @ts-ignore
this.app.commands.executeCommandById("app:reload")
});
}))
a.appendText(` to reload obsidian, or press elsewhere to dismiss this message.`)
}); });
})) });
a.appendText(` to reload obsidian, or press elsewhere to dismiss this message.`) setTrigger("popupUpdated-" + configDir, 1000, () => {
}); //@ts-ignore
}); const isShown = this.confirmPopup?.noticeEl?.isShown();
//@ts-ignore if (!isShown) {
const isShown = this.confirmPopup?.noticeEl?.isShown(); this.confirmPopup = new Notice(fragment, 0);
if (!isShown) { }
this.confirmPopup = new Notice(fragment, 0); setTrigger("popupClose" + configDir, 20000, () => {
this.confirmPopup?.hide();
this.confirmPopup = null;
})
})
}
} }
if (this.confirmPopupTimer != null) {
clearTimeout(this.confirmPopupTimer);
}
setTimeout(() => {
this.confirmPopup?.hide();
this.confirmPopup = null;
}, 10000)
} }
Logger(`Hidden files scanned`, logLevel, "sync_internal"); Logger(`Hidden files scanned: ${filesChanged} files had been modified`, logLevel, "sync_internal");
} }
} }

View File

@@ -12,3 +12,64 @@ export function path2id(filename: string): string {
export function id2path(filename: string): string { export function id2path(filename: string): string {
return id2path_base(normalizePath(filename)); return id2path_base(normalizePath(filename));
} }
const triggers: { [key: string]: ReturnType<typeof setTimeout> } = {};
export function setTrigger(key: string, timeout: number, proc: (() => Promise<any> | void)) {
clearTrigger(key);
triggers[key] = setTimeout(async () => {
delete triggers[key];
await proc();
}, timeout);
}
export function clearTrigger(key: string) {
if (key in triggers) {
clearTimeout(triggers[key]);
}
}
export function clearAllTriggers() {
for (const v in triggers) {
clearTimeout(triggers[v]);
}
}
const intervals: { [key: string]: ReturnType<typeof setInterval> } = {};
export function setPeriodic(key: string, timeout: number, proc: (() => Promise<any> | void)) {
clearPeriodic(key);
intervals[key] = setInterval(async () => {
delete intervals[key];
await proc();
}, timeout);
}
export function clearPeriodic(key: string) {
if (key in intervals) {
clearInterval(intervals[key]);
}
}
export function clearAllPeriodic() {
for (const v in intervals) {
clearInterval(intervals[v]);
}
}
const memos: { [key: string]: any } = {};
export function memoObject<T>(key: string, obj: T): T {
memos[key] = obj;
return memos[key] as T;
}
export async function memoIfNotExist<T>(key: string, func: () => T | Promise<T>): Promise<T> {
if (!(key in memos)) {
const w = func();
const v = w instanceof Promise ? (await w) : w;
memos[key] = v;
}
return memos[key] as T;
}
export function retriveMemoObject<T>(key: string): T | false {
if (key in memos) {
return memos[key];
} else {
return false;
}
}
export function disposeMemoObject(key: string) {
delete memos[key];
}

View File

@@ -93,6 +93,10 @@
padding-left: 4px; padding-left: 4px;
} }
.sls-header-button {
margin-left: 2em;
}
.sls-hidden { .sls-hidden {
display: none; display: none;
} }

12
updates.md Normal file
View File

@@ -0,0 +1,12 @@
### 0.13.0
- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded.
- Please turn on `Delete metadata of deleted files.` if you are using livesync-classroom or filesystem-livesync.
- We can see the history of deleted files.
- `Pick file to show` was renamed to `Pick a file to show.
- Files in the `Pick a file to show` are now ordered by their modified date descent.
- Update information became to be shown on the major upgrade.
#### Minors
- 0.13.1 Fixed on conflict resolution.
- 0.13.2 Fixed file deletion failures.