Compare commits

...

21 Commits

Author SHA1 Message Date
vorotamoroz 331acd463d bump 2022-10-25 11:48:21 +09:00
vorotamoroz 9d4f41bbf9 Fixed failure of detection 2022-10-25 11:47:02 +09:00
vorotamoroz 8831165965 bump 2022-10-25 11:34:09 +09:00
vorotamoroz ed62e9331b Implemented:
- A configuration information reporting tools has been implemented.

Improved:
- Fixed detection of IBM Cloudant
2022-10-25 11:33:37 +09:00
vorotamoroz 799e604eb2 bump 2022-10-21 18:22:02 +09:00
vorotamoroz d9b69d9a1b Fixed:
- Fixed the Infinity loop
2022-10-21 18:20:03 +09:00
vorotamoroz c18b5c24b4 bump 2022-10-14 17:39:16 +09:00
vorotamoroz 07f16e3d7d Added missing log updates. 2022-10-14 17:37:25 +09:00
vorotamoroz 486f1aa4a0 Bump 2022-10-05 17:14:52 +09:00
vorotamoroz 075c6beb68 New feature:
- Monitor hidden files, Now we can use internal file sync without scan.
Fixed:
- Periodic synchronisation sometimes failed.
- Status-display had not been cleared in some cases.
- `Skip patterns default` has been changed to more clear name.
2022-10-05 17:14:32 +09:00
vorotamoroz d6121b0c1e bump 2022-10-03 10:57:39 +09:00
vorotamoroz 3292a48054 Fixed
- The boot sequence has been corrected and now boots smoothly.
- Auto applying of batch save will be processed earlier than before.
2022-10-03 10:52:31 +09:00
vorotamoroz ee37764040 bump 2022-10-02 01:48:50 +09:00
vorotamoroz b6f7fced22 Use new library for batching the chunk retrieving 2022-10-02 01:45:44 +09:00
vorotamoroz 13456c0854 Fixed: deleted debug message 2022-10-02 01:44:39 +09:00
vorotamoroz 2663a52fd7 bump 2022-09-29 16:58:53 +09:00
vorotamoroz d4bbf79514 Fixed:
- Fixed a bug about deleting empty directory
- Weird behaviour on boot-sequence on mobile devices.
2022-09-29 16:58:39 +09:00
vorotamoroz 5f96cc6b82 bump 2022-09-28 17:57:23 +09:00
vorotamoroz 8c8f5d045f Fixed:
- Fixed bug about renaming file
2022-09-28 17:56:34 +09:00
vorotamoroz 40cf8be890 Bump 2022-09-28 16:17:11 +09:00
vorotamoroz 6b03dbbe75 Fixed:
- File tracking logic has been refined.
2022-09-28 16:17:04 +09:00
9 changed files with 414 additions and 230 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Self-hosted LiveSync", "name": "Self-hosted LiveSync",
"version": "0.15.5", "version": "0.16.4",
"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",
+17 -17
View File
@@ -1,12 +1,12 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.15.5", "version": "0.16.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.15.5", "version": "0.16.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
@@ -30,7 +30,7 @@
"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.16.3",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"postcss-load-config": "^3.1.4", "postcss-load-config": "^3.1.4",
"rollup": "^2.32.1", "rollup": "^2.32.1",
@@ -2571,9 +2571,9 @@
} }
}, },
"node_modules/moment": { "node_modules/moment": {
"version": "2.29.3", "version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "*" "node": "*"
@@ -2671,13 +2671,13 @@
} }
}, },
"node_modules/obsidian": { "node_modules/obsidian": {
"version": "0.15.4", "version": "0.16.3",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.15.4.tgz", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.16.3.tgz",
"integrity": "sha512-FE11CxxpVD6t/DBvjLvlT7q7YYW91ubTqPKIIp286LdnyLipS8Xi3Tif8i8ALPv87Vg9obKM43aWcPsYLxLllQ==", "integrity": "sha512-hal9qk1A0GMhHSeLr2/+o3OpLmImiP+Y+sx2ewP13ds76KXsziG96n+IPFT0mSkup1zSwhEu+DeRhmbcyCCXWw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/codemirror": "0.0.108", "@types/codemirror": "0.0.108",
"moment": "2.29.3" "moment": "2.29.4"
}, },
"peerDependencies": { "peerDependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@@ -5490,9 +5490,9 @@
} }
}, },
"moment": { "moment": {
"version": "2.29.3", "version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"dev": true "dev": true
}, },
"ms": { "ms": {
@@ -5560,13 +5560,13 @@
} }
}, },
"obsidian": { "obsidian": {
"version": "0.15.4", "version": "0.16.3",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.15.4.tgz", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.16.3.tgz",
"integrity": "sha512-FE11CxxpVD6t/DBvjLvlT7q7YYW91ubTqPKIIp286LdnyLipS8Xi3Tif8i8ALPv87Vg9obKM43aWcPsYLxLllQ==", "integrity": "sha512-hal9qk1A0GMhHSeLr2/+o3OpLmImiP+Y+sx2ewP13ds76KXsziG96n+IPFT0mSkup1zSwhEu+DeRhmbcyCCXWw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/codemirror": "0.0.108", "@types/codemirror": "0.0.108",
"moment": "2.29.3" "moment": "2.29.4"
} }
}, },
"once": { "once": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.15.5", "version": "0.16.4",
"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",
@@ -27,7 +27,7 @@
"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.16.3",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"postcss-load-config": "^3.1.4", "postcss-load-config": "^3.1.4",
"rollup": "^2.32.1", "rollup": "^2.32.1",
+2 -2
View File
@@ -5,7 +5,7 @@ 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";
export class LocalPouchDB extends LocalPouchDBBase { export class LocalPouchDB extends LocalPouchDBBase {
@@ -77,7 +77,7 @@ export class LocalPouchDB extends LocalPouchDBBase {
const opts_length = opts.body.toString().length; const opts_length = opts.body.toString().length;
if (opts_length > 1024 * 1024 * 10) { if (opts_length > 1024 * 1024 * 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.");
+93 -38
View File
@@ -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;
@@ -964,8 +966,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 +985,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 +1001,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 +1020,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 +1267,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'. ",
+1 -1
Submodule src/lib updated: d8d83b7f46...b8a765f8e8
+251 -143
View File
@@ -35,13 +35,14 @@ 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);
const ICHeader = "i:"; const ICHeader = "i:";
const ICHeaderEnd = "i;"; const ICHeaderEnd = "i;";
const ICHeaderLength = ICHeader.length; const ICHeaderLength = ICHeader.length;
const FileWatchEventQueueMax = 10;
/** /**
* returns is internal chunk of file * returns is internal chunk of file
@@ -98,7 +99,7 @@ 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 : app.vault.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) {
@@ -109,6 +110,19 @@ function recentlyTouched(file: TFile) {
function clearTouched() { function clearTouched() {
touchedFiles = []; touchedFiles = [];
} }
type CacheData = string | ArrayBuffer;
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
type FileEventArgs = {
file: TAbstractFile | InternalFileInfo;
cache?: CacheData;
oldPath?: string;
ctx?: any;
}
type FileEventItem = {
type: FileEventType,
args: FileEventArgs
}
export default class ObsidianLiveSyncPlugin extends Plugin { export default class ObsidianLiveSyncPlugin extends Plugin {
settings: ObsidianLiveSyncSettings; settings: ObsidianLiveSyncSettings;
localDatabase: LocalPouchDB; localDatabase: LocalPouchDB;
@@ -118,6 +132,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
suspended: boolean; suspended: boolean;
deviceAndVaultName: string; deviceAndVaultName: string;
isMobile = false; isMobile = false;
isReady = false;
watchedFileEventQueue = [] as FileEventItem[];
getVaultName(): string { getVaultName(): string {
return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : ""); return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
@@ -277,28 +294,27 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.refreshStatusText = this.refreshStatusText.bind(this); this.refreshStatusText = this.refreshStatusText.bind(this);
this.statusBar2 = this.addStatusBarItem(); this.statusBar2 = this.addStatusBarItem();
// this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false);
// this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false);
// this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false);
this.watchVaultChange = this.watchVaultChange.bind(this); this.watchVaultChange = this.watchVaultChange.bind(this);
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.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()) {
@@ -682,6 +698,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) || "";
} }
@@ -700,12 +720,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
gcTimerHandler: any = null; gcTimerHandler: any = null;
registerFileWatchEvents() {
registerWatchEvents() {
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange)); this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
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() {
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen)); this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
window.addEventListener("visibilitychange", this.watchWindowVisibility); window.addEventListener("visibilitychange", this.watchWindowVisibility);
window.addEventListener("online", this.watchOnline); window.addEventListener("online", this.watchOnline);
@@ -728,6 +752,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async watchWindowVisibilityAsync() { async watchWindowVisibilityAsync() {
if (this.settings.suspendFileWatching) return; if (this.settings.suspendFileWatching) return;
if (!this.isReady) return;
// if (this.suspended) return; // if (this.suspended) return;
const isHidden = document.hidden; const isHidden = document.hidden;
await this.applyBatchChange(); await this.applyBatchChange();
@@ -752,12 +777,143 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
// Cache file and waiting to can be proceed.
async appendWatchEvent(type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string, ctx?: any) {
// check really we can process.
if (file instanceof TFile && !this.isTargetFile(file)) return;
if (this.settings.suspendFileWatching) return;
let cache: null | string | ArrayBuffer;
// new file or something changed, cache the changes.
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
if (recentlyTouched(file)) {
return;
}
if (!isPlainText(file.name)) {
cache = await this.app.vault.readBinary(file);
} else {
// cache = await this.app.vault.read(file);
cache = await this.app.vault.cachedRead(file);
if (!cache) cache = await this.app.vault.read(file);
}
}
if (this.settings.batchSave) {
// if the latest event is the same type, omit that
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
// a.md MODIFY
// a.md CREATE
// :
let i = this.watchedFileEventQueue.length;
while (i >= 0) {
i--;
if (i < 0) break;
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
continue;
}
if (this.watchedFileEventQueue[i].type != type) break;
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
}
}
this.watchedFileEventQueue.push({
type,
args: {
file,
oldPath,
cache,
ctx
}
})
this.refreshStatusText();
if (this.isReady) {
await this.procFileEvent();
}
}
async procFileEvent(applyBatch?: boolean) {
if (!this.isReady) return;
if (this.settings.batchSave) {
if (!applyBatch && this.watchedFileEventQueue.length < FileWatchEventQueueMax) {
// Defer till applying batch save or queue has been grown enough.
// or 120 seconds after.
setTrigger("applyBatchAuto", 30000, () => {
this.procFileEvent(true);
})
return;
}
}
clearTrigger("applyBatchAuto");
const ret = await runWithLock("procFiles", true, async () => {
do {
const procs = [...this.watchedFileEventQueue];
this.watchedFileEventQueue = [];
for (const queue of procs) {
const file = queue.args.file;
const key = `file-last-proc-${queue.type}-${file.path}`;
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
if (file instanceof TFile && file.stat.mtime == last) {
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) {
await this.localDatabase.kvDB.set(key, file.stat.mtime);
}
}
this.refreshStatusText();
} while (this.watchedFileEventQueue.length != 0);
return true;
})
this.refreshStatusText();
return ret;
}
watchVaultCreate(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("CREATE", file, null, ctx);
}
watchVaultChange(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("CHANGED", file, null, ctx);
}
watchVaultDelete(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("DELETE", file, null, ctx);
}
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
this.appendWatchEvent("RENAME", file, oldFile, ctx);
}
watchWorkspaceOpen(file: TFile) { watchWorkspaceOpen(file: TFile) {
if (this.settings.suspendFileWatching) return; if (this.settings.suspendFileWatching) return;
if (!this.isReady) return;
this.watchWorkspaceOpenAsync(file); this.watchWorkspaceOpenAsync(file);
} }
async watchWorkspaceOpenAsync(file: TFile) { async watchWorkspaceOpenAsync(file: TFile) {
if (this.settings.suspendFileWatching) return;
if (!this.isReady) return;
await this.applyBatchChange(); await this.applyBatchChange();
if (file == null) { if (file == null) {
return; return;
@@ -768,102 +924,56 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.showIfConflicted(file); await this.showIfConflicted(file);
} }
watchVaultCreate(file: TFile, ...args: any[]) {
if (!this.isTargetFile(file)) return;
if (this.settings.suspendFileWatching) return;
if (recentlyTouched(file)) {
return;
}
this.watchVaultChangeAsync(file, ...args);
}
watchVaultChange(file: TAbstractFile, ...args: any[]) {
if (!this.isTargetFile(file)) return;
if (!(file instanceof TFile)) {
return;
}
if (recentlyTouched(file)) {
return;
}
if (this.settings.suspendFileWatching) return;
// If batchSave is enabled, queue all changes and do nothing.
if (this.settings.batchSave) {
~(async () => {
const meta = await this.localDatabase.getDBEntryMeta(file.path);
if (meta != false) {
const localMtime = ~~(file.stat.mtime / 1000);
const docMtime = ~~(meta.mtime / 1000);
if (localMtime !== docMtime) {
// Perhaps we have to modify (to using newer doc), but we don't be sure to every device's clock is adjusted.
this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path]));
this.refreshStatusText();
}
}
})();
return;
}
this.watchVaultChangeAsync(file, ...args);
}
async applyBatchChange() { async applyBatchChange() {
if (!this.settings.batchSave || this.batchFileChange.length == 0) { 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; return;
} }
return await runWithLock("batchSave", false, async () => { this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100);
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[]; const id = filename2idInternalChunk(path);
this.batchFileChange = []; const filesOnDB = await this.localDatabase.getDBEntryMeta(id);
const semaphore = Semaphore(3); const dbMTime = ~~((filesOnDB && filesOnDB.mtime || 0) / 1000);
const batchProcesses = batchItems.map(e => (async (e) => { // Skip unchanged file.
const releaser = await semaphore.acquire(1, "batch"); if (dbMTime == storageMTime) {
try { // Logger(`STORAGE --> DB:${path}: (hidden) Nothing changed`);
const f = this.app.vault.getAbstractFileByPath(normalizePath(e));
if (f && f instanceof TFile) {
await this.updateIntoDB(f);
Logger(`Batch save:${e}`);
}
} catch (ex) {
Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.VERBOSE);
} finally {
releaser();
}
})(e))
await Promise.all(batchProcesses);
this.refreshStatusText();
return; return;
});
}
batchFileChange: string[] = [];
async watchVaultChangeAsync(file: TFile, ...args: any[]) {
if (file instanceof TFile) {
if (recentlyTouched(file)) {
return;
}
await this.updateIntoDB(file);
} }
}
watchVaultDelete(file: TAbstractFile) { // Do not compare timestamp. Always local data should be preferred except this plugin wrote one.
if (!this.isTargetFile(file)) return; if (storageMTime == 0) {
// When save is delayed, it should be cancelled. await this.deleteInternalFileOnDatabase(path);
this.batchFileChange = this.batchFileChange.filter((e) => e != file.path); } else {
if (this.settings.suspendFileWatching) return; await this.storeInternalFileToDatabase({ path: path, ...stat });
this.watchVaultDeleteAsync(file).then(() => { });
}
async watchVaultDeleteAsync(file: TAbstractFile) {
if (file instanceof TFile) {
await this.deleteFromDB(file);
} else if (file instanceof TFolder) {
await this.deleteFolderOnDB(file);
} }
} }
GetAllFilesRecursively(file: TAbstractFile): TFile[] { GetAllFilesRecursively(file: TAbstractFile): TFile[] {
if (file instanceof TFile) { if (file instanceof TFile) {
return [file]; return [file];
@@ -879,12 +989,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
watchVaultRename(file: TAbstractFile, oldFile: any) {
if (!this.isTargetFile(file)) return;
if (this.settings.suspendFileWatching) return;
this.watchVaultRenameAsync(file, oldFile).then(() => { });
}
getFilePath(file: TAbstractFile): string { getFilePath(file: TAbstractFile): string {
if (file instanceof TFolder) { if (file instanceof TFolder) {
if (file.isRoot()) return ""; if (file.isRoot()) return "";
@@ -897,13 +1001,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return this.getFilePath(file.parent) + "/" + file.name; return this.getFilePath(file.parent) + "/" + file.name;
} }
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any) { 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.
@@ -924,7 +1023,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} else if (file instanceof TFile) { } else if (file instanceof TFile) {
try { try {
Logger(`file save ${file.path} into db`); Logger(`file save ${file.path} into db`);
await this.updateIntoDB(file); await this.updateIntoDB(file, false, cache);
Logger(`deleted ${oldFile} from db`); Logger(`deleted ${oldFile} from db`);
await this.deleteFromDBbyPath(oldFile); await this.deleteFromDBbyPath(oldFile);
} catch (ex) { } catch (ex) {
@@ -1046,7 +1145,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
ctime: doc.ctime, ctime: doc.ctime,
mtime: doc.mtime, mtime: doc.mtime,
}); });
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path); // this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
Logger(msg + path); Logger(msg + path);
touch(newFile); touch(newFile);
this.app.vault.trigger("create", newFile); this.app.vault.trigger("create", newFile);
@@ -1066,7 +1165,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
ctime: doc.ctime, ctime: doc.ctime,
mtime: doc.mtime, mtime: doc.mtime,
}); });
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path); // this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
Logger(msg + path); Logger(msg + path);
touch(newFile); touch(newFile);
this.app.vault.trigger("create", newFile); this.app.vault.trigger("create", newFile);
@@ -1080,7 +1179,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);
@@ -1134,7 +1235,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.ensureDirectory(path); await this.ensureDirectory(path);
try { try {
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 = this.app.vault.getAbstractFileByPath(file.path) as TFile;
touch(xf); touch(xf);
@@ -1152,7 +1253,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
try { try {
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 = this.app.vault.getAbstractFileByPath(file.path) as TFile;
touch(xf); touch(xf);
this.app.vault.trigger("modify", xf); this.app.vault.trigger("modify", xf);
@@ -1199,6 +1300,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
); );
this.refreshStatusText();
} }
async handleDBChangedAsync(change: EntryBody) { async handleDBChangedAsync(change: EntryBody) {
@@ -1259,6 +1361,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);
@@ -1508,7 +1611,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.statusBar.title = this.localDatabase.syncStatus; this.statusBar.title = this.localDatabase.syncStatus;
let waiting = ""; let waiting = "";
if (this.settings.batchSave) { if (this.settings.batchSave) {
waiting = " " + this.batchFileChange.map((e) => "🛫").join(""); waiting = " " + this.watchedFileEventQueue.map((e) => "🛫").join("");
waiting = waiting.replace(/(🛫){10}/g, "🚀"); waiting = waiting.replace(/(🛫){10}/g, "🚀");
} }
let queued = ""; let queued = "";
@@ -1565,6 +1668,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;
@@ -1574,24 +1678,30 @@ 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);
} }
async initializeDatabase(showingNotice?: boolean) { async initializeDatabase(showingNotice?: boolean) {
this.isReady = false;
if (await this.openDatabase()) { if (await this.openDatabase()) {
if (this.localDatabase.isReady) { if (this.localDatabase.isReady) {
await this.syncAllFiles(showingNotice); await this.syncAllFiles(showingNotice);
} }
this.isReady = true;
// run queued event once.
await this.procFileEvent(true);
return true; return true;
} else { } else {
this.isReady = false;
return false; return false;
} }
} }
async replicateAllToServer(showingNotice?: boolean) { async replicateAllToServer(showingNotice?: boolean) {
if (!this.isReady) return false;
if (this.settings.autoSweepPlugins) { if (this.settings.autoSweepPlugins) {
await this.sweepPlugin(showingNotice); await this.sweepPlugin(showingNotice);
} }
@@ -1680,9 +1790,14 @@ 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(`Check or pull from db:${e}`); const w = await this.localDatabase.getDBEntryMeta(e);
await this.pullFile(e, filesStorage, false, null, false); if (w) {
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 {
Logger(`entry not found, maybe deleted (it is normal behavior):${e}`);
}
}); });
} }
if (!initialScan) { if (!initialScan) {
@@ -1754,23 +1869,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
} }
async renameFolder(folder: TFolder, oldFile: any) {
for (const v of folder.children) {
const entry = v as TFile & TFolder;
if (entry.children) {
await this.deleteFolderOnDB(entry);
if (this.settings.trashInsteadDelete) {
await this.app.vault.trash(entry, false);
} else {
await this.app.vault.delete(entry);
}
} else {
await this.deleteFromDB(entry);
}
}
}
// --> 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 {
@@ -2027,20 +2125,30 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async updateIntoDB(file: TFile, initialScan?: boolean) { async updateIntoDB(file: TFile, initialScan?: boolean, cache?: CacheData) {
if (!this.isTargetFile(file)) return; if (!this.isTargetFile(file)) return;
if (shouldBeIgnored(file.path)) { if (shouldBeIgnored(file.path)) {
return; return;
} }
let content = ""; let content = "";
let datatype: "plain" | "newnote" = "newnote"; let datatype: "plain" | "newnote" = "newnote";
if (!isPlainText(file.name)) { if (!cache) {
const contentBin = await this.app.vault.readBinary(file); if (!isPlainText(file.name)) {
content = await arrayBufferToBase64(contentBin); const contentBin = await this.app.vault.readBinary(file);
datatype = "newnote"; content = await arrayBufferToBase64(contentBin);
datatype = "newnote";
} else {
content = await this.app.vault.read(file);
datatype = "plain";
}
} else { } else {
content = await this.app.vault.read(file); if (cache instanceof ArrayBuffer) {
datatype = "plain"; content = await arrayBufferToBase64(cache);
datatype = "newnote"
} else {
content = cache;
datatype = "plain";
}
} }
const fullPath = path2id(file.path); const fullPath = path2id(file.path);
const d: LoadedEntry = { const d: LoadedEntry = {
@@ -2292,7 +2400,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);
} }
} }
+22 -26
View File
@@ -1,5 +1,19 @@
### 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.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!
@@ -10,30 +24,12 @@ I appreciate for reviewing and giving me advice @Pouhon158!
- 0.15.3 Fixed the issue about locking/unlocking remote database while rebuilding in the wizard. - 0.15.3 Fixed the issue about locking/unlocking remote database while rebuilding in the wizard.
- 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.14.1 - 0.15.7 Fixed bug about renaming file.
- The target selecting filter was implemented. - 0.15.8 Fixed bug about deleting empty directory, weird behaviour on boot-sequence on mobile devices.
Now we can set what files are synchronised by regular expression. - 0.15.9 Improved chunk retrieving, now chunks are retrieved in batch on continuous requests.
- We can configure the size of chunks. - 0.15.10 Fixed:
We can use larger chunks to improve performance. - The boot sequence has been corrected and now boots smoothly.
(This feature can not be used with IBM Cloudant) - Auto applying of batch save will be processed earlier than before.
- 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`.
+25
View File
@@ -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.