Compare commits

...

9 Commits

Author SHA1 Message Date
vorotamoroz
71a80cacc3 bump again 2023-01-19 19:05:16 +09:00
vorotamoroz
38daeca89f fixed leaked logging 2023-01-19 19:03:57 +09:00
vorotamoroz
7ec64a6a93 bump 2023-01-19 18:55:18 +09:00
vorotamoroz
c5c6deb742 Improved:
- Confidential information has no longer stored in data.json as is.
- Synchronising progress has been shown in the notification.
- We can commit passphrases with a keyboard.
- Configuration which had not been saved yet is marked now.

Fixed:
- Hidden files have been synchronised again.

And, minor changes have been included.
2023-01-19 18:50:06 +09:00
vorotamoroz
ef57fbfdda Fixed:
- Now the filename is shown on the Conflict resolving dialog
- Rename of files has been improved again.
2023-01-19 13:11:30 +09:00
vorotamoroz
bc158e9f2b bump 2023-01-17 17:46:06 +09:00
vorotamoroz
6513c53c7e Fixed:
- Document history is now displayed again.

Reorganised:
- Many files have been refactored.
2023-01-17 17:39:26 +09:00
vorotamoroz
5d1074065c bump 2023-01-16 17:33:31 +09:00
vorotamoroz
b444082b0c Fixed:
- Performance improvement
- Now `Chunk size` can be set to under one hundred.

New feature:
- The number of transfers required before replication stabilises is now displayed.
2023-01-16 17:31:37 +09:00
17 changed files with 583 additions and 337 deletions

View File

@@ -1,7 +1,7 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Self-hosted LiveSync", "name": "Self-hosted LiveSync",
"version": "0.17.10", "version": "0.17.15",
"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",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.17.10", "version": "0.17.15",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.17.10", "version": "0.17.15",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.17.10", "version": "0.17.15",
"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",

View File

@@ -1,17 +1,19 @@
import { App, Modal } from "obsidian"; import { App, Modal } from "obsidian";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
import { diff_result } from "./lib/src/types"; import { diff_result } from "./lib/src/types";
import { escapeStringToHTML } from "./lib/src/utils"; import { escapeStringToHTML } from "./lib/src/strbin";
export class ConflictResolveModal extends Modal { export class ConflictResolveModal extends Modal {
// result: Array<[number, string]>; // result: Array<[number, string]>;
result: diff_result; result: diff_result;
filename: string;
callback: (remove_rev: string) => Promise<void>; callback: (remove_rev: string) => Promise<void>;
constructor(app: App, diff: diff_result, callback: (remove_rev: string) => Promise<void>) { constructor(app: App, filename: string, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
super(app); super(app);
this.result = diff; this.result = diff;
this.callback = callback; this.callback = callback;
this.filename = filename;
} }
onOpen() { onOpen() {
@@ -20,6 +22,7 @@ export class ConflictResolveModal extends Modal {
contentEl.empty(); contentEl.empty();
contentEl.createEl("h2", { text: "This document has conflicted changes." }); contentEl.createEl("h2", { text: "This document has conflicted changes." });
contentEl.createEl("span", this.filename);
const div = contentEl.createDiv(""); const div = contentEl.createDiv("");
div.addClass("op-scrollable"); div.addClass("op-scrollable");
let diff = ""; let diff = "";

View File

@@ -1,10 +1,12 @@
import { TFile, Modal, App } from "obsidian"; import { TFile, Modal, App } from "obsidian";
import { path2id } from "./utils"; import { path2id } from "./utils";
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML, isValidPath } from "./lib/src/utils"; import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
import { isValidPath } from "./lib/src/path";
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import { LoadedEntry, LOG_LEVEL } from "./lib/src/types"; import { LoadedEntry, LOG_LEVEL } from "./lib/src/types";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { getDocData } from "./lib/src/utils";
export class DocumentHistoryModal extends Modal { export class DocumentHistoryModal extends Modal {
plugin: ObsidianLiveSyncPlugin; plugin: ObsidianLiveSyncPlugin;
@@ -64,7 +66,7 @@ export class DocumentHistoryModal extends Modal {
this.currentDoc = w; this.currentDoc = w;
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`; this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
let result = ""; let result = "";
const w1data = w.datatype == "plain" ? w.data : base64ToString(w.data); const w1data = w.datatype == "plain" ? getDocData(w.data) : base64ToString(w.data);
this.currentDeleted = w.deleted; this.currentDeleted = w.deleted;
this.currentText = w1data; this.currentText = w1data;
if (this.showDiff) { if (this.showDiff) {
@@ -74,7 +76,7 @@ export class DocumentHistoryModal extends Modal {
const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false, true); const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false, true);
if (w2 != false) { if (w2 != false) {
const dmp = new diff_match_patch(); const dmp = new diff_match_patch();
const w2data = w2.datatype == "plain" ? w2.data : base64ToString(w2.data); const w2data = w2.datatype == "plain" ? getDocData(w2.data) : base64ToString(w2.data);
const diff = dmp.diff_main(w2data, w1data); const diff = dmp.diff_main(w2data, w1data);
dmp.diff_cleanupSemantic(diff); dmp.diff_cleanupSemantic(diff);
for (const v of diff) { for (const v of diff) {
@@ -176,7 +178,7 @@ export class DocumentHistoryModal extends Modal {
Logger("Path is not valid to write content.", LOG_LEVEL.INFO); Logger("Path is not valid to write content.", LOG_LEVEL.INFO);
} }
if (this.currentDoc?.datatype == "plain") { if (this.currentDoc?.datatype == "plain") {
await this.app.vault.adapter.write(pathToWrite, this.currentDoc.data); await this.app.vault.adapter.write(pathToWrite, getDocData(this.currentDoc.data));
await focusFile(pathToWrite); await focusFile(pathToWrite);
this.close(); this.close();
} else if (this.currentDoc?.datatype == "newnote") { } else if (this.currentDoc?.datatype == "newnote") {

View File

@@ -4,7 +4,7 @@ import { LocalPouchDBBase } from "./lib/src/LocalPouchDBBase.js";
import { Logger } from "./lib/src/logger.js"; 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_couchdb.js";
import { isCloudantURI, 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";
@@ -52,7 +52,7 @@ export class LocalPouchDB extends LocalPouchDBBase {
} }
async connectRemoteCouchDB(uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | boolean, useDynamicIterationCount: boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> { async connectRemoteCouchDB(uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid"; if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters."; if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces."; if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
@@ -154,7 +154,7 @@ export class LocalPouchDB extends LocalPouchDBBase {
}; };
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf); const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
if (passphrase && typeof passphrase === "string") { if (passphrase !== "false" && typeof passphrase === "string") {
enableEncryption(db, passphrase, useDynamicIterationCount); enableEncryption(db, passphrase, useDynamicIterationCount);
} }
try { try {

View File

@@ -1,21 +1,17 @@
import { App, Modal } from "obsidian"; import { App, Modal } from "obsidian";
import { escapeStringToHTML } from "./lib/src/utils"; import { logMessageStore } from "./lib/src/stores";
import { escapeStringToHTML } from "./lib/src/strbin";
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
export class LogDisplayModal extends Modal { export class LogDisplayModal extends Modal {
plugin: ObsidianLiveSyncPlugin; plugin: ObsidianLiveSyncPlugin;
logEl: HTMLDivElement; logEl: HTMLDivElement;
unsubscribe: () => void;
constructor(app: App, plugin: ObsidianLiveSyncPlugin) { constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
super(app); super(app);
this.plugin = plugin; this.plugin = plugin;
} }
updateLog() {
let msg = "";
for (const v of this.plugin.logMessage) {
msg += escapeStringToHTML(v) + "<br>";
}
this.logEl.innerHTML = msg;
}
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
@@ -25,13 +21,18 @@ export class LogDisplayModal extends Modal {
div.addClass("op-scrollable"); div.addClass("op-scrollable");
div.addClass("op-pre"); div.addClass("op-pre");
this.logEl = div; this.logEl = div;
this.updateLog = this.updateLog.bind(this); this.unsubscribe = logMessageStore.observe((e) => {
this.plugin.addLogHook = this.updateLog; let msg = "";
this.updateLog(); for (const v of e) {
msg += escapeStringToHTML(v) + "<br>";
}
this.logEl.innerHTML = msg;
})
logMessageStore.invalidate();
} }
onClose() { onClose() {
const { contentEl } = this; const { contentEl } = this;
contentEl.empty(); contentEl.empty();
this.plugin.addLogHook = null; if (this.unsubscribe) this.unsubscribe();
} }
} }

View File

@@ -1,7 +1,9 @@
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "obsidian"; import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "obsidian";
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, RemoteDBSettings } from "./lib/src/types"; import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, ConfigPassphraseStore, RemoteDBSettings } from "./lib/src/types";
import { path2id, id2path } from "./utils"; import { path2id, id2path } from "./utils";
import { delay, Semaphore, versionNumberString2Number } from "./lib/src/utils"; import { delay } from "./lib/src/utils";
import { Semaphore } from "./lib/src/semaphore";
import { versionNumberString2Number } from "./lib/src/strbin";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { checkSyncInfo, isCloudantURI } 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";
@@ -41,6 +43,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
} }
display(): void { display(): void {
const { containerEl } = this; const { containerEl } = this;
let encrypt = this.plugin.settings.encrypt;
let passphrase = this.plugin.settings.passphrase;
let useDynamicIterationCount = this.plugin.settings.useDynamicIterationCount;
containerEl.empty(); containerEl.empty();
@@ -291,68 +296,78 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
) )
); );
new Setting(containerRemoteDatabaseEl) const e2e = new Setting(containerRemoteDatabaseEl)
.setName("End to End Encryption") .setName("End to End Encryption")
.setDesc("Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommend.") .setDesc("Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommend.")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => { toggle.setValue(encrypt).onChange(async (value) => {
if (inWizard) { if (inWizard) {
this.plugin.settings.encrypt = value; this.plugin.settings.encrypt = value;
passphrase.setDisabled(!value); passphraseSetting.setDisabled(!value);
dynamicIteration.setDisabled(!value); dynamicIteration.setDisabled(!value);
await this.plugin.saveSettings(); await this.plugin.saveSettings();
} else { } else {
this.plugin.settings.workingEncrypt = value; encrypt = value;
passphrase.setDisabled(!value); passphraseSetting.setDisabled(!value);
dynamicIteration.setDisabled(!value); dynamicIteration.setDisabled(!value);
await this.plugin.saveSettings(); await this.plugin.saveSettings();
markDirtyControl();
} }
}) })
); );
const passphrase = new Setting(containerRemoteDatabaseEl)
const markDirtyControl = () => {
passphraseSetting.controlEl.toggleClass("sls-item-dirty", passphrase != this.plugin.settings.passphrase);
e2e.controlEl.toggleClass("sls-item-dirty", encrypt != this.plugin.settings.encrypt);
dynamicIteration.controlEl.toggleClass("sls-item-dirty", useDynamicIterationCount != this.plugin.settings.useDynamicIterationCount)
}
const passphraseSetting = new Setting(containerRemoteDatabaseEl)
.setName("Passphrase") .setName("Passphrase")
.setDesc("Encrypting passphrase. If you change the passphrase of a existing database, overwriting the remote database is strongly recommended.") .setDesc("Encrypting passphrase. If you change the passphrase of a existing database, overwriting the remote database is strongly recommended.")
.addText((text) => { .addText((text) => {
text.setPlaceholder("") text.setPlaceholder("")
.setValue(this.plugin.settings.workingPassphrase) .setValue(passphrase)
.onChange(async (value) => { .onChange(async (value) => {
if (inWizard) { if (inWizard) {
this.plugin.settings.passphrase = value; this.plugin.settings.passphrase = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
} else { } else {
this.plugin.settings.workingPassphrase = value; passphrase = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
markDirtyControl();
} }
}); });
text.inputEl.setAttribute("type", "password"); text.inputEl.setAttribute("type", "password");
}); });
passphrase.setDisabled(!this.plugin.settings.workingEncrypt); passphraseSetting.setDisabled(!encrypt);
const dynamicIteration = new Setting(containerRemoteDatabaseEl) const dynamicIteration = new Setting(containerRemoteDatabaseEl)
.setName("Use dynamic iteration count (experimental)") .setName("Use dynamic iteration count (experimental)")
.setDesc("Balancing the encryption/decryption load against the length of the passphrase if toggled. (v0.17.5 or higher required)") .setDesc("Balancing the encryption/decryption load against the length of the passphrase if toggled. (v0.17.5 or higher required)")
.addToggle((toggle) => { .addToggle((toggle) => {
toggle.setValue(this.plugin.settings.workingUseDynamicIterationCount) toggle.setValue(useDynamicIterationCount)
.onChange(async (value) => { .onChange(async (value) => {
if (inWizard) { if (inWizard) {
this.plugin.settings.useDynamicIterationCount = value; this.plugin.settings.useDynamicIterationCount = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
} else { } else {
this.plugin.settings.workingUseDynamicIterationCount = value; useDynamicIterationCount = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
markDirtyControl();
} }
}); });
}) })
.setClass("wizardHidden"); .setClass("wizardHidden");
dynamicIteration.setDisabled(!this.plugin.settings.workingEncrypt); dynamicIteration.setDisabled(!encrypt);
const checkWorkingPassphrase = async (): Promise<boolean> => { const checkWorkingPassphrase = async (): Promise<boolean> => {
const settingForCheck: RemoteDBSettings = { const settingForCheck: RemoteDBSettings = {
...this.plugin.settings, ...this.plugin.settings,
encrypt: this.plugin.settings.workingEncrypt, encrypt: encrypt,
passphrase: this.plugin.settings.workingPassphrase, passphrase: passphrase,
useDynamicIterationCount: this.plugin.settings.workingUseDynamicIterationCount, useDynamicIterationCount: useDynamicIterationCount,
}; };
console.dir(settingForCheck); console.dir(settingForCheck);
const db = await this.plugin.localDatabase.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.localDatabase.isMobile); const db = await this.plugin.localDatabase.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.localDatabase.isMobile);
@@ -370,19 +385,19 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
} }
}; };
const applyEncryption = async (sendToServer: boolean) => { const applyEncryption = async (sendToServer: boolean) => {
if (this.plugin.settings.workingEncrypt && this.plugin.settings.workingPassphrase == "") { if (encrypt && passphrase == "") {
Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE); Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE);
return; return;
} }
if (this.plugin.settings.workingEncrypt && !(await testCrypt())) { if (encrypt && !(await testCrypt())) {
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE); Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE);
return; return;
} }
if (!(await checkWorkingPassphrase()) && !sendToServer) { if (!(await checkWorkingPassphrase()) && !sendToServer) {
return; return;
} }
if (!this.plugin.settings.workingEncrypt) { if (!encrypt) {
this.plugin.settings.workingPassphrase = ""; passphrase = "";
} }
this.plugin.settings.liveSync = false; this.plugin.settings.liveSync = false;
this.plugin.settings.periodicReplication = false; this.plugin.settings.periodicReplication = false;
@@ -390,11 +405,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.syncOnStart = false; this.plugin.settings.syncOnStart = false;
this.plugin.settings.syncOnFileOpen = false; this.plugin.settings.syncOnFileOpen = false;
this.plugin.settings.syncAfterMerge = false; this.plugin.settings.syncAfterMerge = false;
this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt; this.plugin.settings.encrypt = encrypt;
this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase; this.plugin.settings.passphrase = passphrase;
this.plugin.settings.useDynamicIterationCount = this.plugin.settings.workingUseDynamicIterationCount; this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
markDirtyControl();
if (sendToServer) { if (sendToServer) {
await this.plugin.initializeDatabase(true); await this.plugin.initializeDatabase(true);
await this.plugin.markRemoteLocked(); await this.plugin.markRemoteLocked();
@@ -1184,8 +1200,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.customChunkSize + "") .setValue(this.plugin.settings.customChunkSize + "")
.onChange(async (value) => { .onChange(async (value) => {
let v = Number(value); let v = Number(value);
if (isNaN(v) || v < 100) { if (isNaN(v) || v < 1) {
v = 100; v = 1;
} }
this.plugin.settings.customChunkSize = v; this.plugin.settings.customChunkSize = v;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
@@ -1324,6 +1340,45 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
); );
const passphrase_options: Record<ConfigPassphraseStore, string> = {
"": "Default",
LOCALSTORAGE: "Use a custom passphrase",
ASK_AT_LAUNCH: "Ask an passphrase at every launch",
}
new Setting(containerMiscellaneousEl)
.setName("Encrypting sensitive configuration items")
.addDropdown((dropdown) =>
dropdown
.addOptions(passphrase_options)
.setValue(this.plugin.settings.configPassphraseStore)
.onChange(async (value) => {
this.plugin.settings.configPassphraseStore = value as ConfigPassphraseStore;
this.plugin.usedPassphrase = "";
confPassphraseSetting.setDisabled(this.plugin.settings.configPassphraseStore != "LOCALSTORAGE");
await this.plugin.saveSettings();
})
)
.setClass("wizardHidden");
const confPassphrase = localStorage.getItem("ls-setting-passphrase") || "";
const confPassphraseSetting = new Setting(containerMiscellaneousEl)
.setName("Passphrase of sensitive configuration items")
.setDesc("This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.")
.addText((text) => {
text.setPlaceholder("")
.setValue(confPassphrase)
.onChange(async (value) => {
this.plugin.usedPassphrase = "";
localStorage.setItem("ls-setting-passphrase", value);
await this.plugin.saveSettings();
markDirtyControl();
});
text.inputEl.setAttribute("type", "password");
})
.setClass("wizardHidden");
confPassphraseSetting.setDisabled(this.plugin.settings.configPassphraseStore != "LOCALSTORAGE");
const infoApply = containerMiscellaneousEl.createEl("div", { text: `To finish setup, please select one of the presets` }); const infoApply = containerMiscellaneousEl.createEl("div", { text: `To finish setup, please select one of the presets` });
infoApply.addClass("op-warn-info"); infoApply.addClass("op-warn-info");
infoApply.addClass("wizardOnly") infoApply.addClass("wizardOnly")
@@ -1365,7 +1420,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : "self-hosted"; pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : "self-hosted";
pluginConfig.couchDB_USER = REDACTED; pluginConfig.couchDB_USER = REDACTED;
pluginConfig.passphrase = REDACTED; pluginConfig.passphrase = REDACTED;
pluginConfig.workingPassphrase = REDACTED; pluginConfig.encryptedPassphrase = REDACTED;
pluginConfig.encryptedCouchDBConnection = REDACTED;
const msgConfig = `----remote config---- const msgConfig = `----remote config----
${stringifyYaml(responseConfig)} ${stringifyYaml(responseConfig)}

View File

@@ -2,7 +2,7 @@
import ObsidianLiveSyncPlugin from "./main"; import ObsidianLiveSyncPlugin from "./main";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { DevicePluginList, PluginDataEntry } from "./types"; import { DevicePluginList, PluginDataEntry } from "./types";
import { versionNumberString2Number } from "./lib/src/utils"; import { versionNumberString2Number } from "./lib/src/strbin";
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY"; type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";

View File

@@ -52,14 +52,14 @@ export class InputStringDialog extends Modal {
const { contentEl } = this; const { contentEl } = this;
contentEl.createEl("h1", { text: this.title }); contentEl.createEl("h1", { text: this.title });
// For enter to submit
new Setting(contentEl).setName(this.key).addText((text) => const formEl = contentEl.createEl("form");
new Setting(formEl).setName(this.key).addText((text) =>
text.onChange((value) => { text.onChange((value) => {
this.result = value; this.result = value;
}) })
); );
new Setting(formEl).addButton((btn) =>
new Setting(contentEl).addButton((btn) =>
btn btn
.setButtonText("Ok") .setButtonText("Ok")
.setCta() .setCta()

Submodule src/lib updated: 2284678a59...14fecd2411

View File

@@ -1,43 +1,28 @@
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App, } from "obsidian"; import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App } from "obsidian";
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry, SALT_OF_PASSPHRASE, ConfigPassphraseStore, CouchDBConnection } from "./lib/src/types";
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry } from "./lib/src/types"; import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo, queueItem, FileInfo } from "./types";
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo } from "./types"; import { getDocData, isDocContentSame } from "./lib/src/utils";
import { import { Logger } from "./lib/src/logger";
base64ToString,
arrayBufferToBase64,
base64ToArrayBuffer,
isValidPath,
versionNumberString2Number,
runWithLock,
shouldBeIgnored,
getProcessingCounts,
setLockNotifier,
isPlainText,
setNoticeClass,
NewNotice,
getLocks,
WrappedNotice,
Semaphore,
getDocData,
isDocContentSame,
} from "./lib/src/utils";
import { Logger, setLogger } from "./lib/src/logger";
import { LocalPouchDB } from "./LocalPouchDB"; import { LocalPouchDB } from "./LocalPouchDB";
import { LogDisplayModal } from "./LogDisplayModal"; import { LogDisplayModal } from "./LogDisplayModal";
import { ConflictResolveModal } from "./ConflictResolveModal"; import { ConflictResolveModal } from "./ConflictResolveModal";
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab"; import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
import { DocumentHistoryModal } from "./DocumentHistoryModal"; import { DocumentHistoryModal } from "./DocumentHistoryModal";
import { applyPatch, clearAllPeriodic, clearAllTriggers, clearTrigger, disposeMemoObject, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, memoIfNotExist, memoObject, path2id, retrieveMemoObject, setTrigger, tryParseJSON } from "./utils"; import { applyPatch, clearAllPeriodic, clearAllTriggers, clearTrigger, disposeMemoObject, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, memoIfNotExist, memoObject, path2id, retrieveMemoObject, setTrigger, tryParseJSON } from "./utils";
import { decrypt, encrypt } from "./lib/src/e2ee_v2"; import { decrypt, encrypt, tryDecrypt } 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"; import { isCloudantURI } from "./lib/src/utils_couchdb";
import { getGlobalStore, observeStores } from "./lib/src/store";
import { lockStore, logMessageStore, logStore } from "./lib/src/stores";
import { NewNotice, setNoticeClass, WrappedNotice } from "./lib/src/wrapper";
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
import { isPlainText, isValidPath, shouldBeIgnored } from "./lib/src/path";
import { runWithLock } from "./lib/src/lock";
import { Semaphore } from "./lib/src/semaphore";
setNoticeClass(Notice); setNoticeClass(Notice);
@@ -48,6 +33,7 @@ const FileWatchEventQueueMax = 10;
function getAbstractFileByPath(path: string): TAbstractFile | null { function getAbstractFileByPath(path: string): TAbstractFile | null {
// Hidden API but so useful. // Hidden API but so useful.
// @ts-ignore
if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) { if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
// @ts-ignore // @ts-ignore
return app.vault.getAbstractFileByPathInsensitive(path); return app.vault.getAbstractFileByPathInsensitive(path);
@@ -126,19 +112,20 @@ function clearTouched() {
type CacheData = string | ArrayBuffer; type CacheData = string | ArrayBuffer;
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL"; type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
type FileEventArgs = { type FileEventArgs = {
file: TAbstractFile | InternalFileInfo; file: FileInfo | InternalFileInfo;
cache?: CacheData; cache?: CacheData;
oldPath?: string; oldPath?: string;
ctx?: any; ctx?: any;
} }
type FileEventItem = { type FileEventItem = {
type: FileEventType, type: FileEventType,
args: FileEventArgs args: FileEventArgs,
key: string,
} }
export default class ObsidianLiveSyncPlugin extends Plugin { export default class ObsidianLiveSyncPlugin extends Plugin {
settings: ObsidianLiveSyncSettings; settings: ObsidianLiveSyncSettings;
localDatabase: LocalPouchDB; localDatabase: LocalPouchDB;
logMessage: string[] = [];
statusBar: HTMLElement; statusBar: HTMLElement;
statusBar2: HTMLElement; statusBar2: HTMLElement;
suspended: boolean; suspended: boolean;
@@ -285,7 +272,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async onload() { async onload() {
setLogger(this.addLog.bind(this)); // Logger moved to global. logStore.subscribe(e => this.addLog(e.message, e.level, e.key));
Logger("loading plugin"); Logger("loading plugin");
//@ts-ignore //@ts-ignore
const manifestVersion: string = MANIFEST_VERSION || "0.0.0"; const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
@@ -345,7 +332,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.statusBar = this.addStatusBarItem(); this.statusBar = this.addStatusBarItem();
this.statusBar.addClass("syncstatusbar"); this.statusBar.addClass("syncstatusbar");
this.refreshStatusText = this.refreshStatusText.bind(this);
this.statusBar2 = this.addStatusBarItem(); this.statusBar2 = this.addStatusBarItem();
this.watchVaultChange = this.watchVaultChange.bind(this); this.watchVaultChange = this.watchVaultChange.bind(this);
@@ -360,6 +346,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.parseReplicationResult = this.parseReplicationResult.bind(this); this.parseReplicationResult = this.parseReplicationResult.bind(this);
this.setPeriodicSync = this.setPeriodicSync.bind(this); this.setPeriodicSync = this.setPeriodicSync.bind(this);
this.clearPeriodicSync = this.clearPeriodicSync.bind(this);
this.periodicSync = this.periodicSync.bind(this); this.periodicSync = this.periodicSync.bind(this);
this.loadQueuedFiles = this.loadQueuedFiles.bind(this); this.loadQueuedFiles = this.loadQueuedFiles.bind(this);
@@ -411,11 +398,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const configURIBase = "obsidian://setuplivesync?settings="; const configURIBase = "obsidian://setuplivesync?settings=";
this.addCommand({ this.addCommand({
id: "livesync-copysetupuri", id: "livesync-copysetupuri",
name: "Copy setup URI", name: "Copy the setup URI",
callback: async () => { callback: async () => {
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", ""); const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "");
if (encryptingPassphrase === false) return; if (encryptingPassphrase === false) return;
const setting = { ...this.settings }; const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[]; const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
for (const k of keys) { for (const k of keys) {
if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) { if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) {
@@ -430,11 +417,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}); });
this.addCommand({ this.addCommand({
id: "livesync-copysetupurifull", id: "livesync-copysetupurifull",
name: "Copy setup URI (Full)", name: "Copy the setup URI (Full)",
callback: async () => { callback: async () => {
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", ""); const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "");
if (encryptingPassphrase === false) return; if (encryptingPassphrase === false) return;
const setting = { ...this.settings }; const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false)); const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false));
const uri = `${configURIBase}${encryptedSetting}`; const uri = `${configURIBase}${encryptedSetting}`;
await navigator.clipboard.writeText(uri); await navigator.clipboard.writeText(uri);
@@ -443,7 +430,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}); });
this.addCommand({ this.addCommand({
id: "livesync-opensetupuri", id: "livesync-opensetupuri",
name: "Open setup URI", name: "Open the setup URI",
callback: async () => { callback: async () => {
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`); const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
if (setupURI === false) return; if (setupURI === false) return;
@@ -459,16 +446,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const setupWizard = async (confString: string) => { const setupWizard = async (confString: string) => {
try { try {
const oldConf = JSON.parse(JSON.stringify(this.settings)); const oldConf = JSON.parse(JSON.stringify(this.settings));
const encryptingPassphrase = await askString(this.app, "Passphrase", "Passphrase for your settings", ""); const encryptingPassphrase = await askString(this.app, "Passphrase", "The passphrase to decrypt your setup URI", "");
if (encryptingPassphrase === false) return; if (encryptingPassphrase === false) return;
const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false)); const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false));
if (newConf) { if (newConf) {
const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?"); const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?");
if (result == "yes") { if (result == "yes") {
const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf); const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf) as ObsidianLiveSyncSettings;
this.localDatabase.closeReplication(); this.localDatabase.closeReplication();
this.settings.suspendFileWatching = true; this.settings.suspendFileWatching = true;
console.dir(newSettingW); console.dir(newSettingW);
// Back into the default method once.
newSettingW.configPassphraseStore = "";
newSettingW.encryptedPassphrase = "";
newSettingW.encryptedCouchDBConnection = "";
const setupJustImport = "Just import setting"; const setupJustImport = "Just import setting";
const setupAsNew = "Set it up as secondary or subsequent device"; const setupAsNew = "Set it up as secondary or subsequent device";
const setupAgain = "Reconfigure and reconstitute the data"; const setupAgain = "Reconfigure and reconstitute the data";
@@ -477,9 +468,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const setupType = await askSelectString(this.app, "How would you like to set it up?", [setupAsNew, setupAgain, setupJustImport, setupManually]); const setupType = await askSelectString(this.app, "How would you like to set it up?", [setupAsNew, setupAgain, setupJustImport, setupManually]);
if (setupType == setupJustImport) { if (setupType == setupJustImport) {
this.settings = newSettingW; this.settings = newSettingW;
this.usedPassphrase = "";
await this.saveSettings(); await this.saveSettings();
} else if (setupType == setupAsNew) { } else if (setupType == setupAsNew) {
this.settings = newSettingW; this.settings = newSettingW;
this.usedPassphrase = "";
await this.saveSettings(); await this.saveSettings();
await this.resetLocalOldDatabase(); await this.resetLocalOldDatabase();
await this.resetLocalDatabase(); await this.resetLocalDatabase();
@@ -491,6 +484,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (await askSelectString(this.app, "Do you really want to do this?", ["Cancel", confirm]) != confirm) { if (await askSelectString(this.app, "Do you really want to do this?", ["Cancel", confirm]) != confirm) {
return; return;
} }
this.settings = newSettingW;
this.usedPassphrase = "";
await this.saveSettings(); await this.saveSettings();
await this.resetLocalOldDatabase(); await this.resetLocalOldDatabase();
await this.resetLocalDatabase(); await this.resetLocalDatabase();
@@ -507,6 +502,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (keepLocalDB == "yes" && keepRemoteDB == "yes") { if (keepLocalDB == "yes" && keepRemoteDB == "yes") {
// nothing to do. so peaceful. // nothing to do. so peaceful.
this.settings = newSettingW; this.settings = newSettingW;
this.usedPassphrase = "";
await this.saveSettings(); await this.saveSettings();
const replicate = await askYesNo(this.app, "Unlock and replicate?"); const replicate = await askYesNo(this.app, "Unlock and replicate?");
if (replicate == "yes") { if (replicate == "yes") {
@@ -526,6 +522,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
let initDB; let initDB;
this.settings = newSettingW; this.settings = newSettingW;
this.usedPassphrase = "";
await this.saveSettings(); await this.saveSettings();
if (keepLocalDB == "no") { if (keepLocalDB == "no") {
this.resetLocalOldDatabase(); this.resetLocalOldDatabase();
@@ -634,9 +631,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000); this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000);
this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000); this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000);
setLockNotifier(() => {
this.refreshStatusText();
});
this.addCommand({ this.addCommand({
id: "livesync-plugin-dialog", id: "livesync-plugin-dialog",
name: "Show Plugins and their settings", name: "Show Plugins and their settings",
@@ -726,17 +721,97 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//@ts-ignore //@ts-ignore
const isMobile = this.app.isMobile; const isMobile = this.app.isMobile;
this.localDatabase = new LocalPouchDB(this.settings, vaultName, isMobile); this.localDatabase = new LocalPouchDB(this.settings, vaultName, isMobile);
this.localDatabase.updateInfo = () => { this.observeForLogs();
this.refreshStatusText();
};
return await this.localDatabase.initializeDatabase(); return await this.localDatabase.initializeDatabase();
} }
usedPassphrase = "";
getPassphrase(settings: ObsidianLiveSyncSettings) {
const methods: Record<ConfigPassphraseStore, (() => Promise<string | false>)> = {
"": () => Promise.resolve("*"),
"LOCALSTORAGE": () => Promise.resolve(localStorage.getItem("ls-setting-passphrase") ?? false),
"ASK_AT_LAUNCH": () => askString(this.app, "Passphrase", "passphrase", "")
}
const method = settings.configPassphraseStore;
const methodFunc = method in methods ? methods[method] : methods[""];
return methodFunc();
}
async decryptConfigurationItem(encrypted: string, passphrase: string) {
const dec = await tryDecrypt(encrypted, passphrase + SALT_OF_PASSPHRASE, false);
if (dec) {
this.usedPassphrase = passphrase;
return dec;
}
return false;
}
tryDecodeJson(encoded: string | false): object | false {
try {
if (!encoded) return false;
return JSON.parse(encoded);
} catch (ex) {
return false;
}
}
async encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings) {
if (this.usedPassphrase != "") {
return await encrypt(src, this.usedPassphrase + SALT_OF_PASSPHRASE, false);
}
const passphrase = await this.getPassphrase(settings);
if (passphrase === false) {
Logger("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL.URGENT);
return "";
}
const dec = await encrypt(src, passphrase + SALT_OF_PASSPHRASE, false);
if (dec) {
this.usedPassphrase = passphrase;
return dec;
}
return "";
}
async loadSettings() { async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); const settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) as ObsidianLiveSyncSettings;
this.settings.workingEncrypt = this.settings.encrypt; const passphrase = await this.getPassphrase(settings);
this.settings.workingPassphrase = this.settings.passphrase; if (passphrase === false) {
Logger("Could not determine passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL.URGENT);
} else {
if (settings.encryptedCouchDBConnection) {
const keys = ["couchDB_URI", "couchDB_USER", "couchDB_PASSWORD", "couchDB_DBNAME"] as (keyof CouchDBConnection)[];
const decrypted = this.tryDecodeJson(await this.decryptConfigurationItem(settings.encryptedCouchDBConnection, passphrase)) as CouchDBConnection;
if (decrypted) {
for (const key of keys) {
if (key in decrypted) {
settings[key] = decrypted[key]
}
}
} else {
Logger("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL.URGENT);
for (const key of keys) {
settings[key] = "";
}
}
}
if (settings.encrypt && settings.encryptedPassphrase) {
const encrypted = settings.encryptedPassphrase;
const decrypted = await this.decryptConfigurationItem(encrypted, passphrase);
if (decrypted) {
settings.passphrase = decrypted;
} else {
Logger("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL.URGENT);
settings.passphrase = "";
}
}
}
this.settings = settings;
if ("workingEncrypt" in this.settings) delete this.settings.workingEncrypt;
if ("workingPassphrase" in this.settings) delete this.settings.workingPassphrase;
// Delete this feature to avoid problems on mobile. // Delete this feature to avoid problems on mobile.
this.settings.disableRequestURI = true; this.settings.disableRequestURI = true;
@@ -768,7 +843,29 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName(); const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
localStorage.setItem(lsKey, this.deviceAndVaultName || ""); localStorage.setItem(lsKey, this.deviceAndVaultName || "");
await this.saveData(this.settings); const settings = { ...this.settings };
if (this.usedPassphrase == "" && !await this.getPassphrase(settings)) {
Logger("Could not determine passphrase for saving data.json! Our data.json have insecure items!", LOG_LEVEL.NOTICE);
} else {
if (settings.couchDB_PASSWORD != "" || settings.couchDB_URI != "" || settings.couchDB_USER != "" || settings.couchDB_DBNAME) {
const connectionSetting: CouchDBConnection = {
couchDB_DBNAME: settings.couchDB_DBNAME,
couchDB_PASSWORD: settings.couchDB_PASSWORD,
couchDB_URI: settings.couchDB_URI,
couchDB_USER: settings.couchDB_USER,
};
settings.encryptedCouchDBConnection = await this.encryptConfigurationItem(JSON.stringify(connectionSetting), settings);
settings.couchDB_PASSWORD = "";
settings.couchDB_DBNAME = "";
settings.couchDB_URI = "";
settings.couchDB_USER = "";
}
if (settings.encrypt && settings.passphrase != "") {
settings.encryptedPassphrase = await this.encryptConfigurationItem(settings.passphrase, settings);
settings.passphrase = "";
}
}
await this.saveData(settings);
this.localDatabase.settings = this.settings; this.localDatabase.settings = this.settings;
this.triggerRealizeSettingSyncMode(); this.triggerRealizeSettingSyncMode();
} }
@@ -833,58 +930,78 @@ 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 | InternalFileInfo, oldPath?: string, ctx?: any) { async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
// check really we can process. let forcePerform = false;
if (file instanceof TFile && !this.isTargetFile(file)) return; for (const param of params) {
if (this.settings.suspendFileWatching) return; const atomicKey = [0, 0, 0, 0, 0, 0].map(e => `${Math.floor(Math.random() * 100000)}`).join("-");
const type = param.type;
const file = param.file;
const oldPath = param.oldPath;
if (file instanceof TFolder) continue;
if (!this.isTargetFile(file.path)) continue;
if (this.settings.suspendFileWatching) continue;
let cache: null | string | ArrayBuffer; let cache: null | string | ArrayBuffer;
// new file or something changed, cache the changes. // new file or something changed, cache the changes.
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) { if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
if (recentlyTouched(file)) { 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; continue;
} }
if (this.watchedFileEventQueue[i].type != type) break; if (!isPlainText(file.name)) {
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]); 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 (type == "DELETE" || type == "RENAME") {
forcePerform = true;
} }
}
this.watchedFileEventQueue.push({
type, if (this.settings.batchSave) {
args: { // if the latest event is the same type, omit that
file, // a.md MODIFY <- this should be cancelled when a.md MODIFIED
oldPath, // b.md MODIFY <- this should be cancelled when b.md MODIFIED
cache, // a.md MODIFY
ctx // a.md CREATE
// :
let i = this.watchedFileEventQueue.length;
L1:
while (i >= 0) {
i--;
if (i < 0) break L1;
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
continue L1;
}
if (this.watchedFileEventQueue[i].type != type) break L1;
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
}
} }
})
this.refreshStatusText(); const fileInfo = file instanceof TFile ? {
ctime: file.stat.ctime,
mtime: file.stat.mtime,
file: file,
path: file.path,
size: file.stat.size
} as FileInfo : file as InternalFileInfo;
this.watchedFileEventQueue.push({
type,
args: {
file: fileInfo,
oldPath,
cache,
ctx
},
key: atomicKey
})
}
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
if (this.isReady) { if (this.isReady) {
await this.procFileEvent(); await this.procFileEvent(forcePerform);
} }
} }
@@ -902,62 +1019,75 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
clearTrigger("applyBatchAuto"); clearTrigger("applyBatchAuto");
const ret = await runWithLock("procFiles", true, async () => { const ret = await runWithLock("procFiles", true, async () => {
L2:
do { do {
const procs = [...this.watchedFileEventQueue]; const procs = [...this.watchedFileEventQueue];
this.watchedFileEventQueue = []; this.watchedFileEventQueue = [];
for (const queue of procs) {
L1:
do {
const queue = procs.shift();
if (queue === undefined) break L1;
const file = queue.args.file; const file = queue.args.file;
const key = `file-last-proc-${queue.type}-${file.path}`; const key = `file-last-proc-${queue.type}-${file.path}`;
const last = Number(await this.localDatabase.kvDB.get(key) || 0); 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 (queue.type == "DELETE") {
if (file instanceof TFile) { await this.deleteFromDBbyPath(file.path);
await this.deleteFromDB(file); } else if (queue.type == "INTERNAL") {
} 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); await this.watchVaultRawEventsAsync(file.path);
} else {
const targetFile = this.app.vault.getAbstractFileByPath(file.path);
if (!(targetFile instanceof TFile)) {
Logger(`Target file was not found: ${file.path}`, LOG_LEVEL.INFO);
continue L1;
}
//TODO: check from cache time.
if (file.mtime == last) {
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
continue L1;
}
const cache = queue.args.cache;
if (queue.type == "CREATE" || queue.type == "CHANGED") {
if (!await this.updateIntoDB(targetFile, false, cache)) {
Logger(`DB -> STORAGE: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL.INFO);
// cancel running queues and remove one of atomic operation
this.watchedFileEventQueue = [...procs, ...this.watchedFileEventQueue].filter(e => e.key != queue.key);
continue L2;
}
}
if (queue.type == "RENAME") {
// Obsolete
await this.watchVaultRenameAsync(targetFile, queue.args.oldPath);
}
} }
if (file instanceof TFile) { await this.localDatabase.kvDB.set(key, file.mtime);
await this.localDatabase.kvDB.set(key, file.stat.mtime); } while (procs.length > 0);
}
}
this.refreshStatusText();
} while (this.watchedFileEventQueue.length != 0); } while (this.watchedFileEventQueue.length != 0);
return true; return true;
}) })
this.refreshStatusText();
return ret; return ret;
} }
watchVaultCreate(file: TAbstractFile, ctx?: any) { watchVaultCreate(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("CREATE", file, null, ctx); this.appendWatchEvent([{ type: "CREATE", file }], ctx);
} }
watchVaultChange(file: TAbstractFile, ctx?: any) { watchVaultChange(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("CHANGED", file, null, ctx); this.appendWatchEvent([{ type: "CHANGED", file }], ctx);
} }
watchVaultDelete(file: TAbstractFile, ctx?: any) { watchVaultDelete(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("DELETE", file, null, ctx); this.appendWatchEvent([{ type: "DELETE", file }], ctx);
} }
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) { watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
this.appendWatchEvent("RENAME", file, oldFile, ctx); if (file instanceof TFile) {
this.appendWatchEvent([
{ type: "CREATE", file },
{ type: "DELETE", file: { path: oldFile, mtime: file.stat.mtime, ctime: file.stat.ctime, size: file.stat.size, deleted: true } }
], ctx);
}
} }
watchWorkspaceOpen(file: TFile) { watchWorkspaceOpen(file: TFile) {
@@ -994,11 +1124,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
.replace(/\n| /g, "") .replace(/\n| /g, "")
.split(",").filter(e => e).map(e => new RegExp(e)); .split(",").filter(e => e).map(e => new RegExp(e));
if (ignorePatterns.some(e => path.match(e))) return; if (ignorePatterns.some(e => path.match(e))) return;
this.appendWatchEvent("INTERNAL", { path, mtime: 0, ctime: 0, size: 0 }, "", null); this.appendWatchEvent(
[{
type: "INTERNAL",
file: { path, mtime: 0, ctime: 0, size: 0 }
}], null);
} }
recentProcessedInternalFiles = [] as string[]; recentProcessedInternalFiles = [] as string[];
async watchVaultRawEventsAsync(path: string) { async watchVaultRawEventsAsync(path: string) {
const stat = await this.app.vault.adapter.stat(path); const stat = await this.app.vault.adapter.stat(path);
// sometimes folder is coming. // sometimes folder is coming.
if (stat && stat.type != "file") return; if (stat && stat.type != "file") return;
@@ -1056,38 +1189,23 @@ 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, cache?: CacheData) { async watchVaultRenameAsync(file: TFile, oldFile: any, cache?: CacheData) {
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE); Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
if (file instanceof TFolder) { if (file instanceof TFile) {
const newFiles = this.GetAllFilesRecursively(file);
// for guard edge cases. this won't happen and each file's event will be raise.
for (const i of newFiles) {
try {
const newFilePath = normalizePath(this.getFilePath(i));
const newFile = getAbstractFileByPath(newFilePath);
if (newFile instanceof TFile) {
Logger(`save ${newFile.path} into db`);
await this.updateIntoDB(newFile);
}
} catch (ex) {
Logger(ex);
}
}
Logger(`delete below ${oldFile} from db`);
await this.deleteFromDBbyPath(oldFile);
} else if (file instanceof TFile) {
try { try {
Logger(`file save ${file.path} into db`); // Logger(`RENAMING.. ${file.path} into db`);
await this.updateIntoDB(file, false, cache); if (await this.updateIntoDB(file, false, cache)) {
Logger(`deleted ${oldFile} from db`); // Logger(`deleted ${oldFile} from db`);
await this.deleteFromDBbyPath(oldFile); await this.deleteFromDBbyPath(oldFile);
} else {
Logger(`Could not save new file: ${file.path} `, LOG_LEVEL.NOTICE);
}
} catch (ex) { } catch (ex) {
Logger(ex); Logger(ex);
} }
} }
} }
addLogHook: () => void = null;
//--> Basic document Functions //--> Basic document Functions
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {}; notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
@@ -1108,12 +1226,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
const newMessage = timestamp + "->" + messageContent; const newMessage = timestamp + "->" + messageContent;
this.logMessage = [].concat(this.logMessage).concat([newMessage]).slice(-100);
console.log(vaultName + ":" + newMessage); console.log(vaultName + ":" + newMessage);
logMessageStore.apply(e => [...e, newMessage].slice(-100));
this.setStatusBarText(null, messageContent.substring(0, 30)); this.setStatusBarText(null, messageContent.substring(0, 30));
// if (message instanceof Error) {
// console.trace(message);
// }
if (level >= LOG_LEVEL.NOTICE) { if (level >= LOG_LEVEL.NOTICE) {
if (!key) key = messageContent; if (!key) key = messageContent;
@@ -1152,7 +1267,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}; };
} }
} }
if (this.addLogHook != null) this.addLogHook();
} }
async ensureDirectory(fullPath: string) { async ensureDirectory(fullPath: string) {
@@ -1200,7 +1314,6 @@ 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);
Logger(msg + path); Logger(msg + path);
touch(newFile); touch(newFile);
this.app.vault.trigger("create", newFile); this.app.vault.trigger("create", newFile);
@@ -1220,7 +1333,6 @@ 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);
Logger(msg + path); Logger(msg + path);
touch(newFile); touch(newFile);
this.app.vault.trigger("create", newFile); this.app.vault.trigger("create", newFile);
@@ -1243,11 +1355,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} else { } else {
await this.app.vault.delete(file); await this.app.vault.delete(file);
} }
Logger(`deleted:${file.path}`); Logger(`xxx <- STORAGE (deleted) ${file.path}`);
Logger(`other items:${dir.children.length}`); Logger(`files: ${dir.children.length}`);
if (dir.children.length == 0) { if (dir.children.length == 0) {
if (!this.settings.doNotDeleteFolder) { if (!this.settings.doNotDeleteFolder) {
Logger(`all files deleted by replication, so delete dir`); Logger(`All files under the parent directory (${dir}) have been deleted, so delete this one.`);
await this.deleteVaultItem(dir); await this.deleteVaultItem(dir);
} }
} }
@@ -1355,7 +1467,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
); );
this.refreshStatusText();
} }
async handleDBChangedAsync(change: EntryBody) { async handleDBChangedAsync(change: EntryBody) {
@@ -1400,13 +1511,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
queuedFiles: { queuedFiles = [] as queueItem[];
entry: EntryBody; queuedFilesStore = getGlobalStore("queuedFiles", { queuedItems: [] as queueItem[], fileEventItems: [] as FileEventItem[] });
missingChildren: string[];
timeout?: number;
done?: boolean;
warned?: boolean;
}[] = [];
chunkWaitTimeout = 60000; chunkWaitTimeout = 60000;
saveQueuedFiles() { saveQueuedFiles() {
@@ -1433,7 +1539,6 @@ 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);
@@ -1465,6 +1570,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
this.queuedFiles = this.queuedFiles.filter((e) => !e.done); this.queuedFiles = this.queuedFiles.filter((e) => !e.done);
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
this.saveQueuedFiles(); this.saveQueuedFiles();
} }
parseIncomingChunk(chunk: PouchDB.Core.ExistingDocument<EntryDoc>) { parseIncomingChunk(chunk: PouchDB.Core.ExistingDocument<EntryDoc>) {
@@ -1528,7 +1634,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//---> Sync //---> Sync
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> { async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
this.refreshStatusText();
for (const change of docs) { for (const change of docs) {
if (isPluginChunk(change._id)) { if (isPluginChunk(change._id)) {
if (this.settings.notifyPluginOrSettingUpdated) { if (this.settings.notifyPluginOrSettingUpdated) {
@@ -1600,8 +1705,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
setPeriodicSync() { setPeriodicSync() {
this.clearPeriodicSync();
if (this.settings.periodicReplication && this.settings.periodicReplicationInterval > 0) { if (this.settings.periodicReplication && this.settings.periodicReplicationInterval > 0) {
this.clearPeriodicSync();
this.periodicSyncHandler = this.setInterval(async () => await this.periodicSync(), Math.max(this.settings.periodicReplicationInterval, 30) * 1000); this.periodicSyncHandler = this.setInterval(async () => await this.periodicSync(), Math.max(this.settings.periodicReplicationInterval, 30) * 1000);
} }
} }
@@ -1643,7 +1748,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
if (this.settings.liveSync) { if (this.settings.liveSync) {
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult); this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
this.refreshStatusText();
} }
if (this.settings.syncInternalFiles) { if (this.settings.syncInternalFiles) {
await this.syncInternalFilesAndDatabase("safe", false); await this.syncInternalFilesAndDatabase("safe", false);
@@ -1655,63 +1759,80 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
lastMessage = ""; lastMessage = "";
observeForLogs() {
const observer__ = observeStores(this.queuedFilesStore, lockStore);
const observer = observeStores(observer__, this.localDatabase.stat);
observer.observe(e => {
const sent = e.sent;
const arrived = e.arrived;
const maxPullSeq = e.maxPullSeq;
const maxPushSeq = e.maxPushSeq;
const lastSyncPullSeq = e.lastSyncPullSeq;
const lastSyncPushSeq = e.lastSyncPushSeq;
let pushLast = "";
let pullLast = "";
let w = "";
switch (e.syncStatus) {
case "CLOSED":
case "COMPLETED":
case "NOT_CONNECTED":
w = "⏹";
break;
case "STARTED":
w = "🌀";
break;
case "PAUSED":
w = "💤";
break;
case "CONNECTED":
w = "⚡";
pushLast = ((lastSyncPushSeq == 0) ? "" : (lastSyncPushSeq >= maxPushSeq ? " (LIVE)" : ` (${maxPushSeq - lastSyncPushSeq})`));
pullLast = ((lastSyncPullSeq == 0) ? "" : (lastSyncPullSeq >= maxPullSeq ? " (LIVE)" : ` (${maxPullSeq - lastSyncPullSeq})`));
break;
case "ERRORED":
w = "⚠";
break;
default:
w = "?";
}
this.statusBar.title = e.syncStatus;
let waiting = "";
if (this.settings.batchSave) {
waiting = " " + this.watchedFileEventQueue.map((e) => "🛫").join("");
waiting = waiting.replace(/(🛫){10}/g, "🚀");
}
let queued = "";
const queue = Object.entries(e.queuedItems).filter((e) => !e[1].warned);
const queuedCount = queue.length;
if (queuedCount) {
const pieces = queue.map((e) => e[1].missingChildren).reduce((prev, cur) => prev + cur.length, 0);
queued = ` 🧩 ${queuedCount} (${pieces})`;
}
const processes = e.count;
const processesDisp = processes == 0 ? "" : `${processes}`;
const message = `Sync: ${w}${sent}${pushLast}${arrived}${pullLast}${waiting}${processesDisp}${queued}`;
// const locks = getLocks();
const pendingTask = e.pending.length
? "\nPending: " +
Object.entries(e.pending.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number }))
.map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`)
.join(", ")
: "";
const runningTask = e.running.length
? "\nRunning: " +
Object.entries(e.running.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number }))
.map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`)
.join(", ")
: "";
this.setStatusBarText(message + pendingTask + runningTask);
})
}
refreshStatusText() { refreshStatusText() {
const sent = this.localDatabase.docSent; return;
const arrived = this.localDatabase.docArrived;
let w = "";
switch (this.localDatabase.syncStatus) {
case "CLOSED":
case "COMPLETED":
case "NOT_CONNECTED":
w = "⏹";
break;
case "STARTED":
w = "🌀";
break;
case "PAUSED":
w = "💤";
break;
case "CONNECTED":
w = "⚡";
break;
case "ERRORED":
w = "⚠";
break;
default:
w = "?";
}
this.statusBar.title = this.localDatabase.syncStatus;
let waiting = "";
if (this.settings.batchSave) {
waiting = " " + this.watchedFileEventQueue.map((e) => "🛫").join("");
waiting = waiting.replace(/(🛫){10}/g, "🚀");
}
let queued = "";
const queue = Object.entries(this.queuedFiles).filter((e) => !e[1].warned);
const queuedCount = queue.length;
if (queuedCount) {
const pieces = queue.map((e) => e[1].missingChildren).reduce((prev, cur) => prev + cur.length, 0);
queued = ` 🧩 ${queuedCount} (${pieces})`;
}
const processes = getProcessingCounts();
const processesDisp = processes == 0 ? "" : `${processes}`;
const message = `Sync: ${w}${sent}${arrived}${waiting}${processesDisp}${queued}`;
const locks = getLocks();
const pendingTask = locks.pending.length
? "\nPending: " +
Object.entries(locks.pending.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number }))
.map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`)
.join(", ")
: "";
const runningTask = locks.running.length
? "\nRunning: " +
Object.entries(locks.running.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number }))
.map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`)
.join(", ")
: "";
this.setStatusBarText(message + pendingTask + runningTask);
} }
logHideTimer: NodeJS.Timeout = null; logHideTimer: NodeJS.Timeout = null;
@@ -1726,10 +1847,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const root = activeDocument.documentElement; const root = activeDocument.documentElement;
const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`); const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`);
q.forEach(e => e.setAttr("data-log", '' + (newMsg + "\n" + newLog) + '')) q.forEach(e => e.setAttr("data-log", '' + (newMsg + "\n" + newLog) + ''))
// root.style.setProperty("--slsmessage", '"' + (newMsg + "\n" + newLog).split("\n").join("\\a ") + '"');
} else { } else {
const root = activeDocument.documentElement; const root = activeDocument.documentElement;
// root.style.setProperty("--slsmessage", '""');
const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`); const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`);
q.forEach(e => e.setAttr("data-log", '')) q.forEach(e => e.setAttr("data-log", ''))
} }
@@ -2278,7 +2397,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
showMergeDialog(filename: string, 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, filename, conflictCheckResult, async (selected) => {
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, false, 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);
@@ -2460,9 +2579,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async updateIntoDB(file: TFile, initialScan?: boolean, cache?: CacheData, force?: boolean) { async updateIntoDB(file: TFile, initialScan?: boolean, cache?: CacheData, force?: boolean) {
if (!this.isTargetFile(file)) return; if (!this.isTargetFile(file)) return true;
if (shouldBeIgnored(file.path)) { if (shouldBeIgnored(file.path)) {
return; return true;
} }
let content: string | string[]; let content: string | string[];
let datatype: "plain" | "newnote" = "newnote"; let datatype: "plain" | "newnote" = "newnote";
@@ -2524,15 +2643,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
return false; return false;
}); });
if (isNotChanged) return; if (isNotChanged) return true;
await this.localDatabase.putDBEntry(d, initialScan); const ret = await this.localDatabase.putDBEntry(d, initialScan);
this.queuedFiles = this.queuedFiles.map((e) => ({ ...e, ...(e.entry._id == d._id ? { done: true } : {}) })); this.queuedFiles = this.queuedFiles.map((e) => ({ ...e, ...(e.entry._id == d._id ? { done: true } : {}) }));
Logger(msg + fullPath); Logger(msg + fullPath);
if (this.settings.syncOnSave && !this.suspended) { if (this.settings.syncOnSave && !this.suspended) {
await this.replicate(); await this.replicate();
} }
return ret != false;
} }
async deleteFromDB(file: TFile) { async deleteFromDB(file: TFile) {

View File

@@ -1,5 +1,5 @@
import { PluginManifest } from "obsidian"; import { PluginManifest, TFile } from "obsidian";
import { DatabaseEntry } from "./lib/src/types"; import { DatabaseEntry, EntryBody } from "./lib/src/types";
export interface PluginDataEntry extends DatabaseEntry { export interface PluginDataEntry extends DatabaseEntry {
deviceVaultName: string; deviceVaultName: string;
@@ -30,3 +30,20 @@ export interface InternalFileInfo {
size: number; size: number;
deleted?: boolean; deleted?: boolean;
} }
export interface FileInfo {
path: string;
mtime: number;
ctime: number;
size: number;
deleted?: boolean;
file: TFile;
}
export type queueItem = {
entry: EntryBody;
missingChildren: string[];
timeout?: number;
done?: boolean;
warned?: boolean;
};

View File

@@ -1,6 +1,6 @@
import { normalizePath } from "obsidian"; import { normalizePath } from "obsidian";
import { path2id_base, id2path_base } from "./lib/src/utils"; import { path2id_base, id2path_base } from "./lib/src/path";
// For backward compatibility, using the path for determining id. // For backward compatibility, using the path for determining id.
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/". // Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".

View File

@@ -248,3 +248,7 @@ div.sls-setting-menu-btn {
.sls-setting:not(.isWizard) .wizardOnly { .sls-setting:not(.isWizard) .wizardOnly {
display: none; display: none;
} }
.sls-item-dirty::before {
content: "✏";
}

View File

@@ -35,49 +35,38 @@
- Fixed button styling. - Fixed button styling.
- Changed: - Changed:
- Conflict checking on synchronising has been enabled for every note in default. - Conflict checking on synchronising has been enabled for every note in default.
- 0.17.8 - 0.17.8
- Improved: Performance improved. Prebuilt PouchDB is no longer used. - Improved: Performance improved. Prebuilt PouchDB is no longer used.
- Fixed: Merging hidden files is also fixed. - Fixed: Merging hidden files is also fixed.
- New Feature: Now we can synchronise automatically after merging conflicts. - New Feature: Now we can synchronise automatically after merging conflicts.
- 0.17.9 - 0.17.9
- Fixed: Conflict merge of internal files is no longer broken. - Fixed: Conflict merge of internal files is no longer broken.
- Improved: Smoother status display inside the editor. - Improved: Smoother status display inside the editor.
- 0.17.10 - 0.17.10
- Fixed: Large file synchronising has been now addressed! - Fixed: Large file synchronising has been now addressed!
Note: When synchronising large files, we have to set `Chunk size` to lower than 50, disable `Read chunks online`, `Batch size` should be set 50-100, and `Batch limit` could be around 20. Note: When synchronising large files, we have to set `Chunk size` to lower than 50, disable `Read chunks online`, `Batch size` should be set 50-100, and `Batch limit` could be around 20.
### 0.16.0 - 0.17.11
- Now hidden files need not be scanned. Changes will be detected automatically. - Fixed:
- If you want it to back to its previous behaviour, please disable `Monitor changes to internal files`. - Performance improvement
- Due to using an internal API, this feature may become unusable with a major update. If this happens, please disable this once. - Now `Chunk size` can be set to under one hundred.
- New feature:
- The number of transfers required before replication stabilises is now displayed.
- 0.17.12: Skipped.
- 0.17.13
- Fixed: Document history is now displayed again.
- Reorganised: Many files have been refactored.
- 0.17.14: Skipped.
- 0.17.15
- Improved:
- Confidential information has no longer stored in data.json as is.
- Synchronising progress has been shown in the notification.
- We can commit passphrases with a keyboard.
- Configuration which had not been saved yet is marked now.
- Now the filename is shown on the Conflict resolving dialog
- Fixed:
- Hidden files have been synchronised again.
- Rename of files has been fixed again.
#### Minors And, minor changes have been included.
- 0.16.1 Added missing log updates. ... To continue on to `updates_old.md`.
- 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.

View File

@@ -1,3 +1,58 @@
### 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
- Outdated configuration items have been removed.
- Setup wizard has been implemented!
I appreciate for reviewing and giving me advice @Pouhon158!
#### Minors
- 0.15.1 Missed the stylesheet.
- 0.15.2 The wizard has been improved and documented!
- 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.5 Add new features for setting Self-hosted LiveSync up more easier.
- 0.15.6 File tracking logic has been refined.
- 0.15.7 Fixed bug about renaming file.
- 0.15.8 Fixed bug about deleting empty directory, weird behaviour on boot-sequence on mobile devices.
- 0.15.9 Improved chunk retrieving, now chunks are retrieved in batch on continuous requests.
- 0.15.10 Fixed:
- The boot sequence has been corrected and now boots smoothly.
- Auto applying of batch save will be processed earlier than before.
### 0.14.1 ### 0.14.1
- The target selecting filter was implemented. - The target selecting filter was implemented.
Now we can set what files are synchronised by regular expression. Now we can set what files are synchronised by regular expression.