mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-04 06:41:49 +00:00
Compare commits
33 Commits
0.15.6
...
forprivacy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db68bc8e30 | ||
|
|
db3eb7e1a0 | ||
|
|
50f51393fc | ||
|
|
8a04e332d6 | ||
|
|
12ae17aa2f | ||
|
|
657f12f966 | ||
|
|
15a7bed448 | ||
|
|
420c3b94df | ||
|
|
239c087132 | ||
|
|
d1a633c799 | ||
|
|
1c07cd92fc | ||
|
|
adc84d53b1 | ||
|
|
c3a762ceed | ||
|
|
5945638633 | ||
|
|
331acd463d | ||
|
|
9d4f41bbf9 | ||
|
|
8831165965 | ||
|
|
ed62e9331b | ||
|
|
799e604eb2 | ||
|
|
d9b69d9a1b | ||
|
|
c18b5c24b4 | ||
|
|
07f16e3d7d | ||
|
|
486f1aa4a0 | ||
|
|
075c6beb68 | ||
|
|
d6121b0c1e | ||
|
|
3292a48054 | ||
|
|
ee37764040 | ||
|
|
b6f7fced22 | ||
|
|
13456c0854 | ||
|
|
2663a52fd7 | ||
|
|
d4bbf79514 | ||
|
|
5f96cc6b82 | ||
|
|
8c8f5d045f |
@@ -10,9 +10,11 @@ But some additional configurations are required in `local.ini` to use from Self-
|
|||||||
```
|
```
|
||||||
[couchdb]
|
[couchdb]
|
||||||
single_node=true
|
single_node=true
|
||||||
|
max_document_size = 50000000
|
||||||
|
|
||||||
[chttpd]
|
[chttpd]
|
||||||
require_valid_user = true
|
require_valid_user = true
|
||||||
|
max_http_request_size = 4294967296
|
||||||
|
|
||||||
[chttpd_auth]
|
[chttpd_auth]
|
||||||
require_valid_user = true
|
require_valid_user = true
|
||||||
@@ -94,4 +96,4 @@ Using Caddy is a handy way to serve the server with SSL automatically.
|
|||||||
|
|
||||||
I have published [docker-compose.yml and ini files](https://github.com/vrtmrz/self-hosted-livesync-server) that launches Caddy and CouchDB at once. Please try it out.
|
I have published [docker-compose.yml and ini files](https://github.com/vrtmrz/self-hosted-livesync-server) that launches Caddy and CouchDB at once. Please try it out.
|
||||||
|
|
||||||
And, be sure to check the server log and be careful of malicious access.
|
And, be sure to check the server log and be careful of malicious access.
|
||||||
|
|||||||
@@ -11,9 +11,11 @@
|
|||||||
```
|
```
|
||||||
[couchdb]
|
[couchdb]
|
||||||
single_node=true
|
single_node=true
|
||||||
|
max_document_size = 50000000
|
||||||
|
|
||||||
[chttpd]
|
[chttpd]
|
||||||
require_valid_user = true
|
require_valid_user = true
|
||||||
|
max_http_request_size = 4294967296
|
||||||
|
|
||||||
[chttpd_auth]
|
[chttpd_auth]
|
||||||
require_valid_user = true
|
require_valid_user = true
|
||||||
@@ -92,4 +94,4 @@ Note: 不推荐将 CouchDB 挂载到根目录
|
|||||||
|
|
||||||
提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。
|
提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。
|
||||||
|
|
||||||
注意检查服务器日志,当心恶意访问。
|
注意检查服务器日志,当心恶意访问。
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ CouchDBを構築するには、[Dockerのイメージ](https://hub.docker.com/_/
|
|||||||
```
|
```
|
||||||
[couchdb]
|
[couchdb]
|
||||||
single_node=true
|
single_node=true
|
||||||
|
max_document_size = 50000000
|
||||||
|
|
||||||
[chttpd]
|
[chttpd]
|
||||||
require_valid_user = true
|
require_valid_user = true
|
||||||
|
|
||||||
[chttpd_auth]
|
[chttpd_auth]
|
||||||
require_valid_user = true
|
require_valid_user = true
|
||||||
|
max_http_request_size = 4294967296
|
||||||
authentication_redirect = /_utils/session.html
|
authentication_redirect = /_utils/session.html
|
||||||
|
|
||||||
[httpd]
|
[httpd]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ 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 manifestJson = JSON.parse(fs.readFileSync("./manifest.json"));
|
||||||
const packageJson = JSON.parse(fs.readFileSync("./package.json"));
|
const packageJson = JSON.parse(fs.readFileSync("./package.json"));
|
||||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
const updateInfo = JSON.stringify("PATCHED-"+fs.readFileSync("./updates.md") + "");
|
||||||
esbuild
|
esbuild
|
||||||
.build({
|
.build({
|
||||||
banner: {
|
banner: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.15.6",
|
"version": "0.16.8",
|
||||||
"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",
|
||||||
|
|||||||
2807
package-lock.json
generated
2807
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.15.6",
|
"version": "0.16.8",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -13,34 +13,32 @@
|
|||||||
"author": "vorotamoroz",
|
"author": "vorotamoroz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^18.0.0",
|
|
||||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
|
||||||
"@types/diff-match-patch": "^1.0.32",
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
"@types/pouchdb": "^6.4.0",
|
"@types/pouchdb": "^6.4.0",
|
||||||
"@types/pouchdb-browser": "^6.1.3",
|
"@types/pouchdb-browser": "^6.1.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
"@types/xxhashjs": "^0.2.2",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||||
"builtin-modules": "^3.2.0",
|
"@typescript-eslint/parser": "^5.44.0",
|
||||||
"esbuild": "0.13.12",
|
"builtin-modules": "^3.3.0",
|
||||||
"esbuild-svelte": "^0.7.0",
|
"esbuild": "0.15.15",
|
||||||
"eslint": "^7.32.0",
|
"esbuild-svelte": "^0.7.3",
|
||||||
"eslint-config-airbnb-base": "^14.2.1",
|
"eslint": "^8.28.0",
|
||||||
"eslint-plugin-import": "^2.25.2",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"obsidian": "^0.15.4",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"postcss": "^8.4.14",
|
"obsidian": "^0.16.3",
|
||||||
"postcss-load-config": "^3.1.4",
|
"postcss": "^8.4.19",
|
||||||
"rollup": "^2.32.1",
|
"postcss-load-config": "^4.0.1",
|
||||||
"svelte": "^3.49.0",
|
"svelte": "^3.53.1",
|
||||||
"svelte-preprocess": "^4.10.7",
|
"svelte-preprocess": "^4.10.7",
|
||||||
"tslib": "^2.2.0",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^4.2.4"
|
"typescript": "^4.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"esbuild": "0.13.12",
|
"esbuild": "0.15.15",
|
||||||
"esbuild-svelte": "^0.7.0",
|
"esbuild-svelte": "^0.7.3",
|
||||||
"idb": "^7.0.2",
|
"idb": "^7.1.1",
|
||||||
"xxhash-wasm": "^0.4.2"
|
"xxhash-wasm": "^0.4.2",
|
||||||
|
"xxhashjs": "^0.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import typescript from "@rollup/plugin-typescript";
|
|
||||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
|
||||||
import commonjs from "@rollup/plugin-commonjs";
|
|
||||||
|
|
||||||
const isProd = process.env.BUILD === "production";
|
|
||||||
|
|
||||||
const banner = `/*
|
|
||||||
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
|
|
||||||
if you want to view the source visit the plugins github repository
|
|
||||||
*/
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: "./src/main.ts",
|
|
||||||
output: {
|
|
||||||
dir: ".",
|
|
||||||
sourcemap: "inline",
|
|
||||||
sourcemapExcludeSources: isProd,
|
|
||||||
format: "cjs",
|
|
||||||
exports: "default",
|
|
||||||
banner,
|
|
||||||
},
|
|
||||||
external: ["obsidian"],
|
|
||||||
plugins: [
|
|
||||||
typescript({ exclude: ["pouchdb-browser.js", "pouchdb-browser-webpack"] }),
|
|
||||||
nodeResolve({
|
|
||||||
browser: true,
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -38,8 +38,8 @@ export class ConflictResolveModal extends Modal {
|
|||||||
diff = diff.replace(/\n/g, "<br>");
|
diff = diff.replace(/\n/g, "<br>");
|
||||||
div.innerHTML = diff;
|
div.innerHTML = diff;
|
||||||
const div2 = contentEl.createDiv("");
|
const div2 = contentEl.createDiv("");
|
||||||
const date1 = new Date(this.result.left.mtime).toLocaleString();
|
const date1 = new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
||||||
const date2 = new Date(this.result.right.mtime).toLocaleString();
|
const date2 = new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
||||||
div2.innerHTML = `
|
div2.innerHTML = `
|
||||||
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
|
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { Logger } from "./lib/src/logger.js";
|
|||||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||||
import { EntryDoc, LOG_LEVEL } from "./lib/src/types.js";
|
import { EntryDoc, LOG_LEVEL } from "./lib/src/types.js";
|
||||||
import { enableEncryption } from "./lib/src/utils.js";
|
import { enableEncryption } from "./lib/src/utils.js";
|
||||||
import { isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb.js";
|
import { isCloudantURI, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb.js";
|
||||||
import { id2path, path2id } from "./utils.js";
|
import { id2path, path2id } from "./utils.js";
|
||||||
|
import XXH from "xxhashjs";
|
||||||
|
|
||||||
export class LocalPouchDB extends LocalPouchDBBase {
|
export class LocalPouchDB extends LocalPouchDBBase {
|
||||||
|
|
||||||
@@ -33,9 +34,16 @@ export class LocalPouchDB extends LocalPouchDBBase {
|
|||||||
await this.kvDB.destroy();
|
await this.kvDB.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async prepareHashFunctions() {
|
||||||
|
if (this.h32 != null) return;
|
||||||
|
// const { h32, h32Raw } = await xxhash();
|
||||||
|
this.h32 = (input: string, seed: number) => (XXH.h32(input, seed).toString(16))// h32;
|
||||||
|
this.h32Raw = (input: Uint8Array, seed: number) => (XXH.h32(input.buffer, seed).toNumber())// h32;
|
||||||
|
}
|
||||||
|
|
||||||
last_successful_post = false;
|
last_successful_post = false;
|
||||||
getLastPostFailedBySize() {
|
getLastPostFailedBySize() {
|
||||||
return this.last_successful_post;
|
return !this.last_successful_post;
|
||||||
}
|
}
|
||||||
async fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
|
async fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
|
||||||
const ret = await requestUrl(request);
|
const ret = await requestUrl(request);
|
||||||
@@ -75,9 +83,9 @@ export class LocalPouchDB extends LocalPouchDBBase {
|
|||||||
const method = opts.method ?? "GET";
|
const method = opts.method ?? "GET";
|
||||||
if (opts.body) {
|
if (opts.body) {
|
||||||
const opts_length = opts.body.toString().length;
|
const opts_length = opts.body.toString().length;
|
||||||
if (opts_length > 1024 * 1024 * 10) {
|
if (opts_length > 1000 * 1000 * 10) {
|
||||||
// over 10MB
|
// over 10MB
|
||||||
if (uri.contains(".cloudantnosqldb.")) {
|
if (isCloudantURI(uri)) {
|
||||||
this.last_successful_post = false;
|
this.last_successful_post = false;
|
||||||
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
||||||
throw new Error("This request should fail on IBM Cloudant.");
|
throw new Error("This request should fail on IBM Cloudant.");
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer } from "obsidian";
|
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "obsidian";
|
||||||
import { DEFAULT_SETTINGS, LOG_LEVEL, RemoteDBSettings } from "./lib/src/types";
|
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, RemoteDBSettings } from "./lib/src/types";
|
||||||
import { path2id, id2path } from "./utils";
|
import { path2id, id2path } from "./utils";
|
||||||
import { delay, versionNumberString2Number } from "./lib/src/utils";
|
import { delay, versionNumberString2Number } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { checkSyncInfo } from "./lib/src/utils_couchdb.js";
|
import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js";
|
||||||
import { testCrypt } from "./lib/src/e2ee_v2";
|
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
|
const requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, key?: string, body?: string) => {
|
||||||
|
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`));
|
||||||
|
const encoded = window.btoa(utf8str);
|
||||||
|
const authHeader = "Basic " + encoded;
|
||||||
|
// const origin = "capacitor://localhost";
|
||||||
|
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };
|
||||||
|
const uri = `${baseUri}/_node/_local/_config${key ? "/" + key : ""}`;
|
||||||
|
|
||||||
|
const requestParam: RequestUrlParam = {
|
||||||
|
url: uri,
|
||||||
|
method: body ? "PUT" : "GET",
|
||||||
|
headers: transformedHeaders,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
};
|
||||||
|
return await requestUrl(requestParam);
|
||||||
|
};
|
||||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
|
||||||
@@ -67,9 +84,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
element.removeClass("selected");
|
element.removeClass("selected");
|
||||||
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = false;
|
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = false;
|
||||||
});
|
});
|
||||||
console.log(`.sls-setting-label.c-${screen}`)
|
|
||||||
w.querySelectorAll(`.sls-setting-label.c-${screen}`).forEach((element) => {
|
w.querySelectorAll(`.sls-setting-label.c-${screen}`).forEach((element) => {
|
||||||
console.log(element)
|
|
||||||
element.addClass("selected");
|
element.addClass("selected");
|
||||||
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = true;
|
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = true;
|
||||||
});
|
});
|
||||||
@@ -381,15 +396,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.addButton((button) =>
|
.addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText("Apply w/o rebuilding")
|
.setButtonText("Apply w/o rebuilding")
|
||||||
.setWarning()
|
.setWarning()
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.setClass("sls-btn-right")
|
.setClass("sls-btn-right")
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await applyEncryption(false);
|
await applyEncryption(false);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const rebuildDB = async (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") => {
|
const rebuildDB = async (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") => {
|
||||||
@@ -476,23 +491,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
const checkConfig = async () => {
|
const checkConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, key?: string, body?: string) => {
|
if (isCloudantURI(this.plugin.settings.couchDB_URI)) {
|
||||||
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`));
|
Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL.NOTICE);
|
||||||
const encoded = window.btoa(utf8str);
|
return;
|
||||||
const authHeader = "Basic " + encoded;
|
}
|
||||||
// const origin = "capacitor://localhost";
|
|
||||||
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };
|
|
||||||
const uri = `${baseUri}/_node/_local/_config${key ? "/" + key : ""}`;
|
|
||||||
|
|
||||||
const requestParam: RequestUrlParam = {
|
|
||||||
url: uri,
|
|
||||||
method: body ? "PUT" : "GET",
|
|
||||||
headers: transformedHeaders,
|
|
||||||
contentType: "application/json",
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
};
|
|
||||||
return await requestUrl(requestParam);
|
|
||||||
};
|
|
||||||
|
|
||||||
const r = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, window.origin);
|
const r = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, window.origin);
|
||||||
|
|
||||||
@@ -575,7 +577,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
addResult("✔ httpd.enable_cors is ok.");
|
addResult("✔ httpd.enable_cors is ok.");
|
||||||
}
|
}
|
||||||
// If the server is not cloudant, configure request size
|
// If the server is not cloudant, configure request size
|
||||||
if (!this.plugin.settings.couchDB_URI.contains(".cloudantnosqldb.")) {
|
if (!isCloudantURI(this.plugin.settings.couchDB_URI)) {
|
||||||
// REQUEST SIZE
|
// REQUEST SIZE
|
||||||
if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) {
|
if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) {
|
||||||
addResult("❗ chttpd.max_http_request_size is low)");
|
addResult("❗ chttpd.max_http_request_size is low)");
|
||||||
@@ -639,7 +641,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
addResult("--Done--", ["ob-btn-config-head"]);
|
addResult("--Done--", ["ob-btn-config-head"]);
|
||||||
addResult("If you have some trouble with Connection-check even though all Config-check has been passed, Please check your reverse proxy's configuration.", ["ob-btn-config-info"]);
|
addResult("If you have some trouble with Connection-check even though all Config-check has been passed, Please check your reverse proxy's configuration.", ["ob-btn-config-info"]);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Checking configuration failed`);
|
Logger(`Checking configuration failed`, LOG_LEVEL.NOTICE);
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -677,7 +679,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
if (!this.plugin.settings.encrypt) {
|
if (!this.plugin.settings.encrypt) {
|
||||||
this.plugin.settings.passphrase = "";
|
this.plugin.settings.passphrase = "";
|
||||||
}
|
}
|
||||||
if (this.plugin.settings.couchDB_URI.contains(".cloudantnosqldb.")) {
|
if (isCloudantURI(this.plugin.settings.couchDB_URI)) {
|
||||||
this.plugin.settings.customChunkSize = 0;
|
this.plugin.settings.customChunkSize = 0;
|
||||||
} else {
|
} else {
|
||||||
this.plugin.settings.customChunkSize = 100;
|
this.plugin.settings.customChunkSize = 100;
|
||||||
@@ -698,7 +700,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
if (!this.plugin.settings.encrypt) {
|
if (!this.plugin.settings.encrypt) {
|
||||||
this.plugin.settings.passphrase = "";
|
this.plugin.settings.passphrase = "";
|
||||||
}
|
}
|
||||||
if (this.plugin.settings.couchDB_URI.contains(".cloudantnosqldb.")) {
|
if (isCloudantURI(this.plugin.settings.couchDB_URI)) {
|
||||||
this.plugin.settings.customChunkSize = 0;
|
this.plugin.settings.customChunkSize = 0;
|
||||||
} else {
|
} else {
|
||||||
this.plugin.settings.customChunkSize = 100;
|
this.plugin.settings.customChunkSize = 100;
|
||||||
@@ -811,6 +813,24 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
new Setting(containerGeneralSettingsEl)
|
||||||
|
.setName("Delete old metadata of deleted files on start-up")
|
||||||
|
.setClass("wizardHidden")
|
||||||
|
.setDesc("(Days passed, 0 to disable automatic-deletion)")
|
||||||
|
.addText((text) => {
|
||||||
|
text.setPlaceholder("")
|
||||||
|
.setValue(this.plugin.settings.automaticallyDeleteMetadataOfDeletedFiles + "")
|
||||||
|
.onChange(async (value) => {
|
||||||
|
let v = Number(value);
|
||||||
|
if (isNaN(v)) {
|
||||||
|
v = 0;
|
||||||
|
}
|
||||||
|
this.plugin.settings.automaticallyDeleteMetadataOfDeletedFiles = v;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
text.inputEl.setAttribute("type", "number");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
addScreenElement("20", containerGeneralSettingsEl);
|
addScreenElement("20", containerGeneralSettingsEl);
|
||||||
const containerSyncSettingEl = containerEl.createDiv();
|
const containerSyncSettingEl = containerEl.createDiv();
|
||||||
@@ -964,8 +984,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Monitor changes to internal files")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.watchInternalFileChanges).onChange(async (value) => {
|
||||||
|
this.plugin.settings.watchInternalFileChanges = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Scan for hidden files before replication")
|
.setName("Scan for hidden files before replication")
|
||||||
|
.setDesc("This configuration will be ignored if monitoring changes is enabled.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.syncInternalFilesBeforeReplication).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.syncInternalFilesBeforeReplication).onChange(async (value) => {
|
||||||
this.plugin.settings.syncInternalFilesBeforeReplication = value;
|
this.plugin.settings.syncInternalFilesBeforeReplication = value;
|
||||||
@@ -974,7 +1003,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Scan hidden files periodically")
|
.setName("Scan hidden files periodically")
|
||||||
.setDesc("Seconds, 0 to disable.")
|
.setDesc("Seconds, 0 to disable. This configuration will be ignored if monitoring changes is enabled.")
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder("")
|
text.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.syncInternalFilesInterval + "")
|
.setValue(this.plugin.settings.syncInternalFilesInterval + "")
|
||||||
@@ -990,7 +1019,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
let skipPatternTextArea: TextAreaComponent = null;
|
let skipPatternTextArea: TextAreaComponent = null;
|
||||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
||||||
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$";
|
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$";
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Skip patterns")
|
.setName("Skip patterns")
|
||||||
.setDesc(
|
.setDesc(
|
||||||
@@ -1009,7 +1038,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Skip patterns defaults")
|
.setName("Restore the skip pattern to default")
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button.setButtonText("Default")
|
button.setButtonText("Default")
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
@@ -1256,6 +1285,50 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
containerHatchEl.createEl("h3", { text: "Hatch" });
|
containerHatchEl.createEl("h3", { text: "Hatch" });
|
||||||
|
|
||||||
|
|
||||||
|
new Setting(containerHatchEl)
|
||||||
|
.setName("Make report to inform the issue")
|
||||||
|
.setDesc("Verify and repair all files and update database without restoring")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Make report")
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(async () => {
|
||||||
|
let responseConfig: any = {};
|
||||||
|
const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷";
|
||||||
|
try {
|
||||||
|
const r = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, window.origin);
|
||||||
|
|
||||||
|
Logger(JSON.stringify(r.json, null, 2));
|
||||||
|
|
||||||
|
responseConfig = r.json;
|
||||||
|
responseConfig["couch_httpd_auth"].secret = REDACTED;
|
||||||
|
responseConfig["couch_httpd_auth"].authentication_db = REDACTED;
|
||||||
|
responseConfig["couch_httpd_auth"].authentication_redirect = REDACTED;
|
||||||
|
responseConfig["couchdb"].uuid = REDACTED;
|
||||||
|
responseConfig["admins"] = REDACTED;
|
||||||
|
|
||||||
|
} catch (ex) {
|
||||||
|
responseConfig = "Requesting information to the remote CouchDB has been failed. If you are using IBM Cloudant, it is the normal behaviour."
|
||||||
|
}
|
||||||
|
const pluginConfig = JSON.parse(JSON.stringify(this.plugin.settings)) as ObsidianLiveSyncSettings;
|
||||||
|
pluginConfig.couchDB_DBNAME = REDACTED;
|
||||||
|
pluginConfig.couchDB_PASSWORD = REDACTED;
|
||||||
|
pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : "self-hosted";
|
||||||
|
pluginConfig.couchDB_USER = REDACTED;
|
||||||
|
pluginConfig.passphrase = REDACTED;
|
||||||
|
pluginConfig.workingPassphrase = REDACTED;
|
||||||
|
|
||||||
|
const msgConfig = `----remote config----
|
||||||
|
${stringifyYaml(responseConfig)}
|
||||||
|
---- Plug-in config ---
|
||||||
|
${stringifyYaml(pluginConfig)}`;
|
||||||
|
console.log(msgConfig);
|
||||||
|
await navigator.clipboard.writeText(msgConfig);
|
||||||
|
Logger(`Information has been copied to clipboard`, LOG_LEVEL.NOTICE);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (this.plugin.localDatabase.remoteLockedAndDeviceNotAccepted) {
|
if (this.plugin.localDatabase.remoteLockedAndDeviceNotAccepted) {
|
||||||
const c = containerHatchEl.createEl("div", {
|
const c = containerHatchEl.createEl("div", {
|
||||||
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. ",
|
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. ",
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: d8d83b7f46...85bb3556ba
327
src/main.ts
327
src/main.ts
@@ -35,6 +35,7 @@ import { decrypt, encrypt } from "./lib/src/e2ee_v2";
|
|||||||
const isDebug = false;
|
const isDebug = false;
|
||||||
|
|
||||||
import { InputStringDialog, PluginDialogModal, PopoverSelectString } from "./dialogs";
|
import { InputStringDialog, PluginDialogModal, PopoverSelectString } from "./dialogs";
|
||||||
|
import { isCloudantURI } from "./lib/src/utils_couchdb";
|
||||||
|
|
||||||
setNoticeClass(Notice);
|
setNoticeClass(Notice);
|
||||||
|
|
||||||
@@ -43,6 +44,16 @@ const ICHeaderEnd = "i;";
|
|||||||
const ICHeaderLength = ICHeader.length;
|
const ICHeaderLength = ICHeader.length;
|
||||||
const FileWatchEventQueueMax = 10;
|
const FileWatchEventQueueMax = 10;
|
||||||
|
|
||||||
|
function getAbstractFileByPath(path: string): TAbstractFile | null {
|
||||||
|
// Hidden API but so useful.
|
||||||
|
if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
|
||||||
|
// @ts-ignore
|
||||||
|
return app.vault.getAbstractFileByPathInsensitive(path);
|
||||||
|
} else {
|
||||||
|
return app.vault.getAbstractFileByPath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns is internal chunk of file
|
* returns is internal chunk of file
|
||||||
* @param str ID
|
* @param str ID
|
||||||
@@ -96,9 +107,9 @@ const askString = (app: App, title: string, key: string, placeholder: string): P
|
|||||||
};
|
};
|
||||||
let touchedFiles: string[] = [];
|
let touchedFiles: string[] = [];
|
||||||
function touch(file: TFile | string) {
|
function touch(file: TFile | string) {
|
||||||
const f = file instanceof TFile ? file : app.vault.getAbstractFileByPath(file) as TFile;
|
const f = file instanceof TFile ? file : getAbstractFileByPath(file) as TFile;
|
||||||
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
|
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
|
||||||
touchedFiles.push(key);
|
touchedFiles.unshift(key);
|
||||||
touchedFiles = touchedFiles.slice(0, 100);
|
touchedFiles = touchedFiles.slice(0, 100);
|
||||||
}
|
}
|
||||||
function recentlyTouched(file: TFile) {
|
function recentlyTouched(file: TFile) {
|
||||||
@@ -111,9 +122,9 @@ function clearTouched() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CacheData = string | ArrayBuffer;
|
type CacheData = string | ArrayBuffer;
|
||||||
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME";
|
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
||||||
type FileEventArgs = {
|
type FileEventArgs = {
|
||||||
file: TAbstractFile;
|
file: TAbstractFile | InternalFileInfo;
|
||||||
cache?: CacheData;
|
cache?: CacheData;
|
||||||
oldPath?: string;
|
oldPath?: string;
|
||||||
ctx?: any;
|
ctx?: any;
|
||||||
@@ -146,7 +157,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isRedFlagRaised(): boolean {
|
isRedFlagRaised(): boolean {
|
||||||
const redflag = this.app.vault.getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG));
|
const redflag = getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG));
|
||||||
if (redflag != null) {
|
if (redflag != null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -173,7 +184,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
nextKey = `${row.id}\u{10ffff}`;
|
nextKey = `${row.id}\u{10ffff}`;
|
||||||
if (!("type" in doc)) continue;
|
if (!("type" in doc)) continue;
|
||||||
if (doc.type == "newnote" || doc.type == "plain") {
|
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 });
|
notes.push({ path: id2path(doc._id), mtime: doc.mtime });
|
||||||
}
|
}
|
||||||
if (isChunk(nextKey)) {
|
if (isChunk(nextKey)) {
|
||||||
@@ -202,8 +212,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
nextKey = `${row.id}\u{10ffff}`;
|
nextKey = `${row.id}\u{10ffff}`;
|
||||||
if (!("_conflicts" in doc)) continue;
|
if (!("_conflicts" in doc)) continue;
|
||||||
if (isInternalChunk(row.id)) continue;
|
if (isInternalChunk(row.id)) continue;
|
||||||
if (doc._deleted) continue;
|
// We have to check also deleted files.
|
||||||
if ("deleted" in doc && doc.deleted) continue;
|
// if (doc._deleted) continue;
|
||||||
|
// if ("deleted" in doc && doc.deleted) continue;
|
||||||
if (doc.type == "newnote" || doc.type == "plain") {
|
if (doc.type == "newnote" || doc.type == "plain") {
|
||||||
// const docId = doc._id.startsWith("i:") ? doc._id.substring("i:".length) : doc._id;
|
// const docId = doc._id.startsWith("i:") ? doc._id.substring("i:".length) : doc._id;
|
||||||
notes.push({ path: id2path(doc._id), mtime: doc.mtime });
|
notes.push({ path: id2path(doc._id), mtime: doc.mtime });
|
||||||
@@ -225,11 +236,52 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (isInternalChunk(target)) {
|
if (isInternalChunk(target)) {
|
||||||
//NOP
|
//NOP
|
||||||
} else {
|
} else {
|
||||||
await this.showIfConflicted(this.app.vault.getAbstractFileByPath(target) as TFile);
|
await this.showIfConflicted(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async collectDeletedFiles() {
|
||||||
|
const pageLimit = 1000;
|
||||||
|
let nextKey = "";
|
||||||
|
const limitDays = this.settings.automaticallyDeleteMetadataOfDeletedFiles;
|
||||||
|
if (limitDays <= 0) return;
|
||||||
|
Logger(`Checking expired file history`);
|
||||||
|
const limit = Date.now() - (86400 * 1000 * limitDays);
|
||||||
|
const notes: { path: string, mtime: number, ttl: number, doc: PouchDB.Core.ExistingDocument<EntryDoc & PouchDB.Core.AllDocsMeta> }[] = [];
|
||||||
|
do {
|
||||||
|
const docs = await this.localDatabase.localDatabase.allDocs({ limit: pageLimit, startkey: nextKey, conflicts: true, include_docs: true });
|
||||||
|
nextKey = "";
|
||||||
|
for (const row of docs.rows) {
|
||||||
|
const doc = row.doc;
|
||||||
|
nextKey = `${row.id}\u{10ffff}`;
|
||||||
|
if (doc.type == "newnote" || doc.type == "plain") {
|
||||||
|
if (doc.deleted && (doc.mtime - limit) < 0) {
|
||||||
|
notes.push({ path: id2path(doc._id), mtime: doc.mtime, ttl: (doc.mtime - limit) / 1000 / 86400, doc: doc });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isChunk(nextKey)) {
|
||||||
|
// skip the chunk zone.
|
||||||
|
nextKey = CHeaderEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (nextKey != "");
|
||||||
|
if (notes.length == 0) {
|
||||||
|
Logger("There are no old documents");
|
||||||
|
Logger(`Checking expired file history done`);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const v of notes) {
|
||||||
|
Logger(`Deletion history expired: ${v.path}`);
|
||||||
|
const delDoc = v.doc;
|
||||||
|
delDoc._deleted = true;
|
||||||
|
// console.dir(delDoc);
|
||||||
|
await this.localDatabase.localDatabase.put(delDoc);
|
||||||
|
}
|
||||||
|
Logger(`Checking expired file history done`);
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
@@ -297,21 +349,23 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||||
this.watchVaultRename = this.watchVaultRename.bind(this);
|
this.watchVaultRename = this.watchVaultRename.bind(this);
|
||||||
|
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
|
||||||
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false);
|
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false);
|
||||||
this.watchWindowVisibility = debounce(this.watchWindowVisibility.bind(this), 1000, false);
|
this.watchWindowVisibility = debounce(this.watchWindowVisibility.bind(this), 1000, false);
|
||||||
this.watchOnline = debounce(this.watchOnline.bind(this), 500, false);
|
this.watchOnline = debounce(this.watchOnline.bind(this), 500, false);
|
||||||
|
|
||||||
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
||||||
|
|
||||||
this.periodicSync = this.periodicSync.bind(this);
|
|
||||||
this.setPeriodicSync = this.setPeriodicSync.bind(this);
|
this.setPeriodicSync = this.setPeriodicSync.bind(this);
|
||||||
|
this.periodicSync = this.periodicSync.bind(this);
|
||||||
|
this.loadQueuedFiles = this.loadQueuedFiles.bind(this);
|
||||||
|
|
||||||
this.getPluginList = this.getPluginList.bind(this);
|
this.getPluginList = this.getPluginList.bind(this);
|
||||||
// this.registerWatchEvents();
|
// this.registerWatchEvents();
|
||||||
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
||||||
|
|
||||||
this.registerFileWatchEvents();
|
|
||||||
this.app.workspace.onLayoutReady(async () => {
|
this.app.workspace.onLayoutReady(async () => {
|
||||||
|
this.registerFileWatchEvents();
|
||||||
if (this.localDatabase.isReady)
|
if (this.localDatabase.isReady)
|
||||||
try {
|
try {
|
||||||
if (this.isRedFlagRaised()) {
|
if (this.isRedFlagRaised()) {
|
||||||
@@ -525,7 +579,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
id: "livesync-checkdoc-conflicted",
|
id: "livesync-checkdoc-conflicted",
|
||||||
name: "Resolve if conflicted.",
|
name: "Resolve if conflicted.",
|
||||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
||||||
await this.showIfConflicted(view.file);
|
await this.showIfConflicted(view.file.path);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -695,6 +749,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.settings.deviceAndVaultName = "";
|
this.settings.deviceAndVaultName = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isCloudantURI(this.settings.couchDB_URI) && this.settings.customChunkSize != 0) {
|
||||||
|
Logger("Configuration verification founds problems with your configuration. This has been fixed automatically. But you may already have data that cannot be synchronised. If this is the case, please rebuild everything.", LOG_LEVEL.NOTICE)
|
||||||
|
this.settings.customChunkSize = 0;
|
||||||
|
}
|
||||||
this.deviceAndVaultName = localStorage.getItem(lsKey) || "";
|
this.deviceAndVaultName = localStorage.getItem(lsKey) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,6 +776,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
||||||
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
||||||
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
|
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
|
||||||
|
//@ts-ignore : Internal API
|
||||||
|
this.registerEvent(this.app.vault.on("raw", this.watchVaultRawEvents));
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWatchEvents() {
|
registerWatchEvents() {
|
||||||
@@ -769,9 +829,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache file and waiting to can be proceed.
|
// Cache file and waiting to can be proceed.
|
||||||
async appendWatchEvent(type: FileEventType, file: TAbstractFile, oldPath?: string, ctx?: any) {
|
async appendWatchEvent(type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string, ctx?: any) {
|
||||||
// check really we can process.
|
// check really we can process.
|
||||||
if (!this.isTargetFile(file)) return;
|
if (file instanceof TFile && !this.isTargetFile(file)) return;
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
|
|
||||||
let cache: null | string | ArrayBuffer;
|
let cache: null | string | ArrayBuffer;
|
||||||
@@ -830,34 +890,52 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (!applyBatch && this.watchedFileEventQueue.length < FileWatchEventQueueMax) {
|
if (!applyBatch && this.watchedFileEventQueue.length < FileWatchEventQueueMax) {
|
||||||
// Defer till applying batch save or queue has been grown enough.
|
// Defer till applying batch save or queue has been grown enough.
|
||||||
// or 120 seconds after.
|
// or 120 seconds after.
|
||||||
setTrigger("applyBatchAuto", 120000, () => {
|
setTrigger("applyBatchAuto", 30000, () => {
|
||||||
this.procFileEvent(true);
|
this.procFileEvent(true);
|
||||||
})
|
})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearTrigger("applyBatchAuto");
|
clearTrigger("applyBatchAuto");
|
||||||
const ret = await runWithLock("procFiles", false, async () => {
|
const ret = await runWithLock("procFiles", true, async () => {
|
||||||
const procs = [...this.watchedFileEventQueue];
|
do {
|
||||||
this.watchedFileEventQueue = [];
|
const procs = [...this.watchedFileEventQueue];
|
||||||
for (const queue of procs) {
|
this.watchedFileEventQueue = [];
|
||||||
const file = queue.args.file;
|
for (const queue of procs) {
|
||||||
const cache = queue.args.cache;
|
const file = queue.args.file;
|
||||||
if ((queue.type == "CREATE" || queue.type == "CHANGED") && file instanceof TFile) {
|
const key = `file-last-proc-${queue.type}-${file.path}`;
|
||||||
await this.updateIntoDB(file, false, cache);
|
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
|
||||||
}
|
if (file instanceof TFile && file.stat.mtime == last) {
|
||||||
if (queue.type == "DELETE") {
|
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = queue.args.cache;
|
||||||
|
if ((queue.type == "CREATE" || queue.type == "CHANGED") && file instanceof TFile) {
|
||||||
|
await this.updateIntoDB(file, false, cache);
|
||||||
|
}
|
||||||
|
if (queue.type == "DELETE") {
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
await this.deleteFromDB(file);
|
||||||
|
} else if (file instanceof TFolder) {
|
||||||
|
await this.deleteFolderOnDB(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (queue.type == "RENAME") {
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
await this.watchVaultRenameAsync(file, queue.args.oldPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (queue.type == "INTERNAL") {
|
||||||
|
await this.watchVaultRawEventsAsync(file.path);
|
||||||
|
}
|
||||||
if (file instanceof TFile) {
|
if (file instanceof TFile) {
|
||||||
await this.deleteFromDB(file);
|
await this.localDatabase.kvDB.set(key, file.stat.mtime);
|
||||||
} else if (file instanceof TFolder) {
|
|
||||||
await this.deleteFolderOnDB(file);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (queue.type == "RENAME") {
|
this.refreshStatusText();
|
||||||
await this.watchVaultRenameAsync(file, queue.args.oldPath);
|
} while (this.watchedFileEventQueue.length != 0);
|
||||||
}
|
return true;
|
||||||
}
|
|
||||||
this.refreshStatusText();
|
|
||||||
})
|
})
|
||||||
this.refreshStatusText();
|
this.refreshStatusText();
|
||||||
return ret;
|
return ret;
|
||||||
@@ -894,13 +972,59 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (this.settings.syncOnFileOpen && !this.suspended) {
|
if (this.settings.syncOnFileOpen && !this.suspended) {
|
||||||
await this.replicate();
|
await this.replicate();
|
||||||
}
|
}
|
||||||
await this.showIfConflicted(file);
|
await this.showIfConflicted(file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyBatchChange() {
|
async applyBatchChange() {
|
||||||
await this.procFileEvent(true);
|
if (this.settings.batchSave) {
|
||||||
|
return await this.procFileEvent(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch raw events (Internal API)
|
||||||
|
watchVaultRawEvents(path: string) {
|
||||||
|
if (!this.settings.syncInternalFiles) return;
|
||||||
|
if (!this.settings.watchInternalFileChanges) return;
|
||||||
|
if (!path.startsWith(this.app.vault.configDir)) return;
|
||||||
|
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns.toLocaleLowerCase()
|
||||||
|
.replace(/\n| /g, "")
|
||||||
|
.split(",").filter(e => e).map(e => new RegExp(e));
|
||||||
|
if (ignorePatterns.some(e => path.match(e))) return;
|
||||||
|
this.appendWatchEvent("INTERNAL", { path, mtime: 0, ctime: 0, size: 0 }, "", null);
|
||||||
|
}
|
||||||
|
recentProcessedInternalFiles = [] as string[];
|
||||||
|
async watchVaultRawEventsAsync(path: string) {
|
||||||
|
|
||||||
|
const stat = await this.app.vault.adapter.stat(path);
|
||||||
|
// sometimes folder is coming.
|
||||||
|
if (stat && stat.type != "file") return;
|
||||||
|
const storageMTime = ~~((stat && stat.mtime || 0) / 1000);
|
||||||
|
const key = `${path}-${storageMTime}`;
|
||||||
|
if (this.recentProcessedInternalFiles.contains(key)) {
|
||||||
|
//If recently processed, it may caused by self.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100);
|
||||||
|
const id = filename2idInternalChunk(path);
|
||||||
|
const filesOnDB = await this.localDatabase.getDBEntryMeta(id);
|
||||||
|
const dbMTime = ~~((filesOnDB && filesOnDB.mtime || 0) / 1000);
|
||||||
|
|
||||||
|
// Skip unchanged file.
|
||||||
|
if (dbMTime == storageMTime) {
|
||||||
|
// Logger(`STORAGE --> DB:${path}: (hidden) Nothing changed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not compare timestamp. Always local data should be preferred except this plugin wrote one.
|
||||||
|
if (storageMTime == 0) {
|
||||||
|
await this.deleteInternalFileOnDatabase(path);
|
||||||
|
} else {
|
||||||
|
await this.storeInternalFileToDatabase({ path: path, ...stat });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
GetAllFilesRecursively(file: TAbstractFile): TFile[] {
|
GetAllFilesRecursively(file: TAbstractFile): TFile[] {
|
||||||
if (file instanceof TFile) {
|
if (file instanceof TFile) {
|
||||||
return [file];
|
return [file];
|
||||||
@@ -930,18 +1054,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any, cache?: CacheData) {
|
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any, cache?: CacheData) {
|
||||||
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
|
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||||
try {
|
|
||||||
await this.applyBatchChange();
|
|
||||||
} catch (ex) {
|
|
||||||
Logger(ex);
|
|
||||||
}
|
|
||||||
if (file instanceof TFolder) {
|
if (file instanceof TFolder) {
|
||||||
const newFiles = this.GetAllFilesRecursively(file);
|
const newFiles = this.GetAllFilesRecursively(file);
|
||||||
// for guard edge cases. this won't happen and each file's event will be raise.
|
// for guard edge cases. this won't happen and each file's event will be raise.
|
||||||
for (const i of newFiles) {
|
for (const i of newFiles) {
|
||||||
try {
|
try {
|
||||||
const newFilePath = normalizePath(this.getFilePath(i));
|
const newFilePath = normalizePath(this.getFilePath(i));
|
||||||
const newFile = this.app.vault.getAbstractFileByPath(newFilePath);
|
const newFile = getAbstractFileByPath(newFilePath);
|
||||||
if (newFile instanceof TFile) {
|
if (newFile instanceof TFile) {
|
||||||
Logger(`save ${newFile.path} into db`);
|
Logger(`save ${newFile.path} into db`);
|
||||||
await this.updateIntoDB(newFile);
|
await this.updateIntoDB(newFile);
|
||||||
@@ -1102,7 +1221,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
touch(newFile);
|
touch(newFile);
|
||||||
this.app.vault.trigger("create", newFile);
|
this.app.vault.trigger("create", newFile);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(msg + "ERROR, Could not parse: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
|
Logger(msg + "ERROR, Could not create: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
|
||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1111,7 +1230,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteVaultItem(file: TFile | TFolder) {
|
async deleteVaultItem(file: TFile | TFolder) {
|
||||||
if (!this.isTargetFile(file)) return;
|
if (file instanceof TFile) {
|
||||||
|
if (!this.isTargetFile(file)) return;
|
||||||
|
}
|
||||||
const dir = file.parent;
|
const dir = file.parent;
|
||||||
if (this.settings.trashInsteadDelete) {
|
if (this.settings.trashInsteadDelete) {
|
||||||
await this.app.vault.trash(file, false);
|
await this.app.vault.trash(file, false);
|
||||||
@@ -1167,7 +1288,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
// this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||||
Logger(msg + path);
|
Logger(msg + path);
|
||||||
const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile;
|
const xf = getAbstractFileByPath(file.path) as TFile;
|
||||||
touch(xf);
|
touch(xf);
|
||||||
this.app.vault.trigger("modify", xf);
|
this.app.vault.trigger("modify", xf);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -1184,7 +1305,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger(msg + path);
|
Logger(msg + path);
|
||||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
// this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||||
const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile;
|
const xf = getAbstractFileByPath(file.path) as TFile;
|
||||||
touch(xf);
|
touch(xf);
|
||||||
this.app.vault.trigger("modify", xf);
|
this.app.vault.trigger("modify", xf);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -1230,10 +1351,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
this.refreshStatusText();
|
||||||
}
|
}
|
||||||
async handleDBChangedAsync(change: EntryBody) {
|
async handleDBChangedAsync(change: EntryBody) {
|
||||||
|
|
||||||
const targetFile = this.app.vault.getAbstractFileByPath(id2path(change._id));
|
const targetFile = getAbstractFileByPath(id2path(change._id));
|
||||||
if (targetFile == null) {
|
if (targetFile == null) {
|
||||||
if (change._deleted || change.deleted) {
|
if (change._deleted || change.deleted) {
|
||||||
return;
|
return;
|
||||||
@@ -1290,6 +1412,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.syncInternalFilesAndDatabase("pull", false, false, w);
|
await this.syncInternalFilesAndDatabase("pull", false, false, w);
|
||||||
Logger(`Applying hidden ${w.length} files changed`);
|
Logger(`Applying hidden ${w.length} files changed`);
|
||||||
});
|
});
|
||||||
|
this.refreshStatusText();
|
||||||
}
|
}
|
||||||
procInternalFile(filename: string) {
|
procInternalFile(filename: string) {
|
||||||
this.procInternalFiles.push(filename);
|
this.procInternalFiles.push(filename);
|
||||||
@@ -1348,7 +1471,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (!this.isTargetFile(id2path(doc._id))) return;
|
if (!this.isTargetFile(id2path(doc._id))) return;
|
||||||
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||||
if ((!isInternalChunk(doc._id)) && skipOldFile) {
|
if ((!isInternalChunk(doc._id)) && skipOldFile) {
|
||||||
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
|
const info = getAbstractFileByPath(id2path(doc._id));
|
||||||
|
|
||||||
if (info && info instanceof TFile) {
|
if (info && info instanceof TFile) {
|
||||||
const localMtime = ~~((info as TFile).stat.mtime / 1000);
|
const localMtime = ~~((info as TFile).stat.mtime / 1000);
|
||||||
@@ -1596,6 +1719,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
updateStatusBarText() { }
|
updateStatusBarText() { }
|
||||||
|
|
||||||
async replicate(showMessage?: boolean) {
|
async replicate(showMessage?: boolean) {
|
||||||
|
if (!this.isReady) return;
|
||||||
if (this.settings.versionUpFlash != "") {
|
if (this.settings.versionUpFlash != "") {
|
||||||
Logger("Open settings and check message, please.", LOG_LEVEL.NOTICE);
|
Logger("Open settings and check message, please.", LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
@@ -1605,7 +1729,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
await this.loadQueuedFiles();
|
await this.loadQueuedFiles();
|
||||||
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication) {
|
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication && !this.settings.watchInternalFileChanges) {
|
||||||
await this.syncInternalFilesAndDatabase("push", showMessage);
|
await this.syncInternalFilesAndDatabase("push", showMessage);
|
||||||
}
|
}
|
||||||
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
||||||
@@ -1654,10 +1778,18 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger("Initializing", LOG_LEVEL.NOTICE, "syncAll");
|
Logger("Initializing", LOG_LEVEL.NOTICE, "syncAll");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.collectDeletedFiles();
|
||||||
|
|
||||||
const filesStorage = this.app.vault.getFiles().filter(e => this.isTargetFile(e));
|
const filesStorage = this.app.vault.getFiles().filter(e => this.isTargetFile(e));
|
||||||
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) => !isChunk(e.id) && !isPluginChunk(e.id) && e.id != "obsydian_livesync_version").filter(e => isValidPath(e.id)).map((e) => id2path(e.id)).filter(e => this.isTargetFile(e));
|
const filesDatabase = wf.rows.filter((e) =>
|
||||||
|
!isChunk(e.id) &&
|
||||||
|
!isPluginChunk(e.id) &&
|
||||||
|
e.id != "obsydian_livesync_version" &&
|
||||||
|
e.id != "_design/replicate"
|
||||||
|
)
|
||||||
|
.filter(e => isValidPath(e.id)).map((e) => id2path(e.id)).filter(e => this.isTargetFile(e));
|
||||||
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) {
|
||||||
@@ -1675,28 +1807,28 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
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;
|
||||||
const semaphore = Semaphore(10);
|
const semaphore = Semaphore(10);
|
||||||
|
|
||||||
Logger(`${procedureName} exec.`);
|
// Logger(`${procedureName} exec.`);
|
||||||
if (!this.localDatabase.isReady) throw Error("Database is not ready!");
|
if (!this.localDatabase.isReady) throw Error("Database is not ready!");
|
||||||
const processes = objects.map(e => (async (v) => {
|
const processes = objects.map(e => (async (v) => {
|
||||||
const releaser = await semaphore.acquire(1, procedureName);
|
const releaser = await semaphore.acquire(1, procedureName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await callback(v);
|
await callback(v);
|
||||||
i++;
|
// i++;
|
||||||
if (i % 50 == 0) {
|
// if (i % 50 == 0) {
|
||||||
const notify = `${procedureName} : ${i}/${count}`;
|
// const notify = `${procedureName} : ${i}/${count}`;
|
||||||
if (showingNotice) {
|
// if (showingNotice) {
|
||||||
Logger(notify, LOG_LEVEL.NOTICE, "syncAll");
|
// Logger(notify, LOG_LEVEL.NOTICE, "syncAll");
|
||||||
} else {
|
// } else {
|
||||||
Logger(notify);
|
// Logger(notify);
|
||||||
}
|
// }
|
||||||
this.setStatusBarText(notify);
|
// this.setStatusBarText(notify);
|
||||||
}
|
// }
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Error while ${procedureName}`, LOG_LEVEL.NOTICE);
|
Logger(`Error while ${procedureName}`, LOG_LEVEL.NOTICE);
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
@@ -1712,14 +1844,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
||||||
Logger(`Update into ${e.path}`);
|
Logger(`Update into ${e.path}`);
|
||||||
|
|
||||||
await this.updateIntoDB(e, initialScan);
|
await this.updateIntoDB(e, initialScan);
|
||||||
});
|
});
|
||||||
if (!initialScan) {
|
if (!initialScan) {
|
||||||
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
||||||
Logger(`Check or pull from db:${e}`);
|
const w = await this.localDatabase.getDBEntryMeta(e, {}, true);
|
||||||
await this.pullFile(e, filesStorage, false, null, false);
|
if (w && !(w.deleted || w._deleted)) {
|
||||||
Logger(`Check or pull from db:${e} OK`);
|
Logger(`Check or pull from db:${e}`);
|
||||||
|
await this.pullFile(e, filesStorage, false, null, false);
|
||||||
|
Logger(`Check or pull from db:${e} OK`);
|
||||||
|
} else if (w) {
|
||||||
|
Logger(`Deletion history skipped: ${e}`, LOG_LEVEL.VERBOSE);
|
||||||
|
} else {
|
||||||
|
Logger(`entry not found: ${e}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!initialScan) {
|
if (!initialScan) {
|
||||||
@@ -1794,7 +1932,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
// --> conflict resolving
|
// --> conflict resolving
|
||||||
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
|
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
|
||||||
try {
|
try {
|
||||||
const doc = await this.localDatabase.getDBEntry(path, { rev: rev }, false, false);
|
const doc = await this.localDatabase.getDBEntry(path, { rev: rev }, false, false, true);
|
||||||
if (doc === false) return false;
|
if (doc === false) return false;
|
||||||
let data = doc.data;
|
let data = doc.data;
|
||||||
if (doc.datatype == "newnote") {
|
if (doc.datatype == "newnote") {
|
||||||
@@ -1803,6 +1941,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
data = doc.data;
|
data = doc.data;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
deleted: doc.deleted || doc._deleted,
|
||||||
ctime: doc.ctime,
|
ctime: doc.ctime,
|
||||||
mtime: doc.mtime,
|
mtime: doc.mtime,
|
||||||
rev: rev,
|
rev: rev,
|
||||||
@@ -1822,7 +1961,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
* @returns true -> resolved, false -> nothing to do, or check result.
|
* @returns true -> resolved, false -> nothing to do, or check result.
|
||||||
*/
|
*/
|
||||||
async getConflictedStatus(path: string): Promise<diff_check_result> {
|
async getConflictedStatus(path: string): Promise<diff_check_result> {
|
||||||
const test = await this.localDatabase.getDBEntry(path, { conflicts: true }, false, false);
|
const test = await this.localDatabase.getDBEntry(path, { conflicts: true }, false, false, true);
|
||||||
if (test === false) return false;
|
if (test === false) return false;
|
||||||
if (test == null) return false;
|
if (test == null) return false;
|
||||||
if (!test._conflicts) return false;
|
if (!test._conflicts) return false;
|
||||||
@@ -1842,8 +1981,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL.NOTICE);
|
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL.NOTICE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// first,check for same contents
|
// first, check for same contents and deletion status.
|
||||||
if (leftLeaf.data == rightLeaf.data) {
|
if (leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted) {
|
||||||
let leaf = leftLeaf;
|
let leaf = leftLeaf;
|
||||||
if (leftLeaf.mtime > rightLeaf.mtime) {
|
if (leftLeaf.mtime > rightLeaf.mtime) {
|
||||||
leaf = rightLeaf;
|
leaf = rightLeaf;
|
||||||
@@ -1877,11 +2016,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
showMergeDialog(file: TFile, conflictCheckResult: diff_result): Promise<boolean> {
|
showMergeDialog(filename: string, conflictCheckResult: diff_result): Promise<boolean> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
|
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
|
||||||
new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => {
|
new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => {
|
||||||
const testDoc = await this.localDatabase.getDBEntry(file.path, { conflicts: true });
|
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, false, true);
|
||||||
if (testDoc === false) {
|
if (testDoc === false) {
|
||||||
Logger("Missing file..", LOG_LEVEL.VERBOSE);
|
Logger("Missing file..", LOG_LEVEL.VERBOSE);
|
||||||
return res(true);
|
return res(true);
|
||||||
@@ -1896,25 +2035,31 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
//concat both,
|
//concat both,
|
||||||
// write data,and delete both old rev.
|
// write data,and delete both old rev.
|
||||||
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
||||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev });
|
await this.localDatabase.deleteDBEntry(filename, { rev: conflictCheckResult.left.rev });
|
||||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev });
|
await this.localDatabase.deleteDBEntry(filename, { rev: conflictCheckResult.right.rev });
|
||||||
await this.app.vault.modify(file, p);
|
const file = getAbstractFileByPath(filename) as TFile;
|
||||||
await this.updateIntoDB(file);
|
if (file) {
|
||||||
await this.pullFile(file.path);
|
await this.app.vault.modify(file, p);
|
||||||
|
await this.updateIntoDB(file);
|
||||||
|
} else {
|
||||||
|
const newFile = await this.app.vault.create(filename, p);
|
||||||
|
await this.updateIntoDB(newFile);
|
||||||
|
}
|
||||||
|
await this.pullFile(filename);
|
||||||
Logger("concat both file");
|
Logger("concat both file");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
//resolved, check again.
|
//resolved, check again.
|
||||||
this.showIfConflicted(file);
|
this.showIfConflicted(filename);
|
||||||
}, 500);
|
}, 500);
|
||||||
} else if (toDelete == null) {
|
} else if (toDelete == null) {
|
||||||
Logger("Leave it still conflicted");
|
Logger("Leave it still conflicted");
|
||||||
} else {
|
} else {
|
||||||
Logger(`Conflict resolved:${file.path}`);
|
Logger(`Conflict resolved:${filename}`);
|
||||||
await this.localDatabase.deleteDBEntry(file.path, { rev: toDelete });
|
await this.localDatabase.deleteDBEntry(filename, { rev: toDelete });
|
||||||
await this.pullFile(file.path, null, true, toKeep);
|
await this.pullFile(filename, null, true, toKeep);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
//resolved, check again.
|
//resolved, check again.
|
||||||
this.showIfConflicted(file);
|
this.showIfConflicted(filename);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1939,9 +2084,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const checkFiles = JSON.parse(JSON.stringify(this.conflictedCheckFiles)) as string[];
|
const checkFiles = JSON.parse(JSON.stringify(this.conflictedCheckFiles)) as string[];
|
||||||
for (const filename of checkFiles) {
|
for (const filename of checkFiles) {
|
||||||
try {
|
try {
|
||||||
const file = this.app.vault.getAbstractFileByPath(filename);
|
const file = getAbstractFileByPath(filename);
|
||||||
if (file != null && file instanceof TFile) {
|
if (file != null && file instanceof TFile) {
|
||||||
await this.showIfConflicted(file);
|
await this.showIfConflicted(file.path);
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
@@ -1950,9 +2095,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async showIfConflicted(file: TFile) {
|
async showIfConflicted(filename: string) {
|
||||||
await runWithLock("conflicted", false, async () => {
|
await runWithLock("conflicted", false, async () => {
|
||||||
const conflictCheckResult = await this.getConflictedStatus(file.path);
|
const conflictCheckResult = await this.getConflictedStatus(filename);
|
||||||
if (conflictCheckResult === false) {
|
if (conflictCheckResult === false) {
|
||||||
//nothing to do.
|
//nothing to do.
|
||||||
return;
|
return;
|
||||||
@@ -1961,17 +2106,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
//auto resolved, but need check again;
|
//auto resolved, but need check again;
|
||||||
Logger("conflict:Automatically merged, but we have to check it again");
|
Logger("conflict:Automatically merged, but we have to check it again");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.showIfConflicted(file);
|
this.showIfConflicted(filename);
|
||||||
}, 500);
|
}, 500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//there conflicts, and have to resolve ;
|
//there conflicts, and have to resolve ;
|
||||||
await this.showMergeDialog(file, conflictCheckResult);
|
await this.showMergeDialog(filename, conflictCheckResult);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
||||||
const targetFile = this.app.vault.getAbstractFileByPath(id2path(filename));
|
const targetFile = getAbstractFileByPath(id2path(filename));
|
||||||
if (!this.isTargetFile(id2path(filename))) return;
|
if (!this.isTargetFile(id2path(filename))) return;
|
||||||
if (targetFile == null) {
|
if (targetFile == null) {
|
||||||
//have to create;
|
//have to create;
|
||||||
@@ -2002,7 +2147,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
throw new Error(`Missing doc:${(file as any).path}`)
|
throw new Error(`Missing doc:${(file as any).path}`)
|
||||||
}
|
}
|
||||||
if (!(file instanceof TFile) && "path" in file) {
|
if (!(file instanceof TFile) && "path" in file) {
|
||||||
const w = this.app.vault.getAbstractFileByPath((file as any).path);
|
const w = getAbstractFileByPath((file as any).path);
|
||||||
if (w instanceof TFile) {
|
if (w instanceof TFile) {
|
||||||
file = w;
|
file = w;
|
||||||
} else {
|
} else {
|
||||||
@@ -2322,7 +2467,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (this.periodicInternalFileScanHandler != null) {
|
if (this.periodicInternalFileScanHandler != null) {
|
||||||
this.clearInternalFileScan();
|
this.clearInternalFileScan();
|
||||||
}
|
}
|
||||||
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval > 0) {
|
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval > 0 && !this.settings.watchInternalFileChanges) {
|
||||||
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicInternalFileScan(), this.settings.syncInternalFilesInterval * 1000);
|
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicInternalFileScan(), this.settings.syncInternalFilesInterval * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
updates.md
67
updates.md
@@ -1,5 +1,39 @@
|
|||||||
|
### 0.16.0
|
||||||
|
- Now hidden files need not be scanned. Changes will be detected automatically.
|
||||||
|
- If you want it to back to its previous behaviour, please disable `Monitor changes to internal files`.
|
||||||
|
- Due to using an internal API, this feature may become unusable with a major update. If this happens, please disable this once.
|
||||||
|
|
||||||
|
#### Minors
|
||||||
|
|
||||||
|
- 0.16.1 Added missing log updates.
|
||||||
|
- 0.16.2 Fixed many problems caused by combinations of `Sync On Save` and the tracking logic that changed at 0.15.6.
|
||||||
|
- 0.16.3
|
||||||
|
- Fixed detection of IBM Cloudant (And if there are some issues, be fixed automatically).
|
||||||
|
- A configuration information reporting tool has been implemented.
|
||||||
|
- 0.16.4 Fixed detection failure. Please set the `Chunk size` again when using a self-hosted database.
|
||||||
|
- 0.16.5
|
||||||
|
- Fixed
|
||||||
|
- Conflict detection and merging now be able to treat deleted files.
|
||||||
|
- Logs while the boot-up sequence has been tidied up.
|
||||||
|
- Fixed incorrect log entries.
|
||||||
|
- New Feature
|
||||||
|
- The feature of automatically deleting old expired metadata has been implemented.
|
||||||
|
We can configure it in `Delete old metadata of deleted files on start-up` in the `General Settings` pane.
|
||||||
|
- 0.16.6
|
||||||
|
- Fixed
|
||||||
|
- Automatic (temporary) batch size adjustment has been restored to work correctly.
|
||||||
|
- Chunk splitting has been backed to the previous behaviour for saving them correctly.
|
||||||
|
- Improved
|
||||||
|
- Corrupted chunks will be detected automatically.
|
||||||
|
- Now on the case-insensitive system, `aaa.md` and `AAA.md` will be treated as the same file or path at applying changesets.
|
||||||
|
- 0.16.7 Nothing has been changed except toolsets, framework library, and as like them. Please inform me if something had been getting strange!
|
||||||
|
- 0.16.8 Now we can synchronise without `bad_request:invalid UTF-8 JSON` even while end-to-end encryption has been disabled.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Before 0.16.5, LiveSync had some issues making chunks. In this case, synchronisation had became been always failing after a corrupted one should be made. After 0.16.6, the corrupted chunk is automatically detected. Sorry for troubling you but please do `rebuild everything` when this plug-in notified so.
|
||||||
|
|
||||||
### 0.15.0
|
### 0.15.0
|
||||||
- Outdated configuration items have been removed.
|
- Outdated configuration items have been removed.
|
||||||
- Setup wizard has been implemented!
|
- Setup wizard has been implemented!
|
||||||
|
|
||||||
I appreciate for reviewing and giving me advice @Pouhon158!
|
I appreciate for reviewing and giving me advice @Pouhon158!
|
||||||
@@ -11,30 +45,11 @@ I appreciate for reviewing and giving me advice @Pouhon158!
|
|||||||
- 0.15.4 Fixed issues about asynchronous processing (e.g., Conflict check or hidden file detection)
|
- 0.15.4 Fixed issues about asynchronous processing (e.g., Conflict check or hidden file detection)
|
||||||
- 0.15.5 Add new features for setting Self-hosted LiveSync up more easier.
|
- 0.15.5 Add new features for setting Self-hosted LiveSync up more easier.
|
||||||
- 0.15.6 File tracking logic has been refined.
|
- 0.15.6 File tracking logic has been refined.
|
||||||
|
- 0.15.7 Fixed bug about renaming file.
|
||||||
### 0.14.1
|
- 0.15.8 Fixed bug about deleting empty directory, weird behaviour on boot-sequence on mobile devices.
|
||||||
- The target selecting filter was implemented.
|
- 0.15.9 Improved chunk retrieving, now chunks are retrieved in batch on continuous requests.
|
||||||
Now we can set what files are synchronised by regular expression.
|
- 0.15.10 Fixed:
|
||||||
- We can configure the size of chunks.
|
- The boot sequence has been corrected and now boots smoothly.
|
||||||
We can use larger chunks to improve performance.
|
- Auto applying of batch save will be processed earlier than before.
|
||||||
(This feature can not be used with IBM Cloudant)
|
|
||||||
- Read chunks online.
|
|
||||||
Now we can synchronise only metadata and retrieve chunks on demand. It reduces local database size and time for replication.
|
|
||||||
- Added this note.
|
|
||||||
- Use local chunks in preference to remote them if present,
|
|
||||||
|
|
||||||
#### Recommended configuration for Self-hosted CouchDB
|
|
||||||
- Set chunk size to around 100 to 250 (10MB - 25MB per chunk)
|
|
||||||
- *Set batch size to 100 and batch limit to 20 (0.14.2)*
|
|
||||||
- Be sure to `Read chunks online` checked.
|
|
||||||
|
|
||||||
#### Minors
|
|
||||||
- 0.14.2 Fixed issue about retrieving files if synchronisation has been interrupted or failed
|
|
||||||
- 0.14.3 New test items have been added to `Check database configuration`.
|
|
||||||
- 0.14.4 Fixed issue of importing configurations.
|
|
||||||
- 0.14.5 Auto chunk size adjusting implemented.
|
|
||||||
- 0.14.6 Change Target to ES2018
|
|
||||||
- 0.14.7 Refactor and fix typos.
|
|
||||||
- 0.14.8 Refactored again. There should be no change in behaviour, but please let me know if there is any.
|
|
||||||
|
|
||||||
... To continue on to `updates_old.md`.
|
... To continue on to `updates_old.md`.
|
||||||
@@ -1,3 +1,28 @@
|
|||||||
|
### 0.14.1
|
||||||
|
- The target selecting filter was implemented.
|
||||||
|
Now we can set what files are synchronised by regular expression.
|
||||||
|
- We can configure the size of chunks.
|
||||||
|
We can use larger chunks to improve performance.
|
||||||
|
(This feature can not be used with IBM Cloudant)
|
||||||
|
- Read chunks online.
|
||||||
|
Now we can synchronise only metadata and retrieve chunks on demand. It reduces local database size and time for replication.
|
||||||
|
- Added this note.
|
||||||
|
- Use local chunks in preference to remote them if present,
|
||||||
|
|
||||||
|
#### Recommended configuration for Self-hosted CouchDB
|
||||||
|
- Set chunk size to around 100 to 250 (10MB - 25MB per chunk)
|
||||||
|
- *Set batch size to 100 and batch limit to 20 (0.14.2)*
|
||||||
|
- Be sure to `Read chunks online` checked.
|
||||||
|
|
||||||
|
#### Minors
|
||||||
|
- 0.14.2 Fixed issue about retrieving files if synchronisation has been interrupted or failed
|
||||||
|
- 0.14.3 New test items have been added to `Check database configuration`.
|
||||||
|
- 0.14.4 Fixed issue of importing configurations.
|
||||||
|
- 0.14.5 Auto chunk size adjusting implemented.
|
||||||
|
- 0.14.6 Change Target to ES2018
|
||||||
|
- 0.14.7 Refactor and fix typos.
|
||||||
|
- 0.14.8 Refactored again. There should be no change in behaviour, but please let me know if there is any.
|
||||||
|
|
||||||
### 0.13.0
|
### 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.
|
- 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user