mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-10 20:08:48 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ec64a6a93 | ||
|
|
c5c6deb742 | ||
|
|
ef57fbfdda | ||
|
|
bc158e9f2b | ||
|
|
6513c53c7e | ||
|
|
5d1074065c | ||
|
|
b444082b0c |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.17.10",
|
||||
"version": "0.17.14",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.17.10",
|
||||
"version": "0.17.14",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.17.10",
|
||||
"version": "0.17.14",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.17.10",
|
||||
"version": "0.17.14",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||
import { diff_result } from "./lib/src/types";
|
||||
import { escapeStringToHTML } from "./lib/src/utils";
|
||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||
|
||||
export class ConflictResolveModal extends Modal {
|
||||
// result: Array<[number, string]>;
|
||||
result: diff_result;
|
||||
filename: string;
|
||||
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);
|
||||
this.result = diff;
|
||||
this.callback = callback;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
@@ -20,6 +22,7 @@ export class ConflictResolveModal extends Modal {
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", { text: "This document has conflicted changes." });
|
||||
contentEl.createEl("span", this.filename);
|
||||
const div = contentEl.createDiv("");
|
||||
div.addClass("op-scrollable");
|
||||
let diff = "";
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { TFile, Modal, App } from "obsidian";
|
||||
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 { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||
import { LoadedEntry, LOG_LEVEL } from "./lib/src/types";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { getDocData } from "./lib/src/utils";
|
||||
|
||||
export class DocumentHistoryModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
@@ -64,7 +66,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.currentDoc = w;
|
||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||
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.currentText = w1data;
|
||||
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);
|
||||
if (w2 != false) {
|
||||
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);
|
||||
dmp.diff_cleanupSemantic(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);
|
||||
}
|
||||
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);
|
||||
this.close();
|
||||
} else if (this.currentDoc?.datatype == "newnote") {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LocalPouchDBBase } from "./lib/src/LocalPouchDBBase.js";
|
||||
import { Logger } from "./lib/src/logger.js";
|
||||
import { PouchDB } from "./lib/src/pouchdb-browser.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 { 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 (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.";
|
||||
@@ -154,7 +154,7 @@ export class LocalPouchDB extends LocalPouchDBBase {
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
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";
|
||||
|
||||
export class LogDisplayModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
logEl: HTMLDivElement;
|
||||
unsubscribe: () => void;
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
updateLog() {
|
||||
let msg = "";
|
||||
for (const v of this.plugin.logMessage) {
|
||||
msg += escapeStringToHTML(v) + "<br>";
|
||||
}
|
||||
this.logEl.innerHTML = msg;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
@@ -25,13 +21,18 @@ export class LogDisplayModal extends Modal {
|
||||
div.addClass("op-scrollable");
|
||||
div.addClass("op-pre");
|
||||
this.logEl = div;
|
||||
this.updateLog = this.updateLog.bind(this);
|
||||
this.plugin.addLogHook = this.updateLog;
|
||||
this.updateLog();
|
||||
this.unsubscribe = logMessageStore.observe((e) => {
|
||||
let msg = "";
|
||||
for (const v of e) {
|
||||
msg += escapeStringToHTML(v) + "<br>";
|
||||
}
|
||||
this.logEl.innerHTML = msg;
|
||||
})
|
||||
logMessageStore.invalidate();
|
||||
}
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.plugin.addLogHook = null;
|
||||
if (this.unsubscribe) this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 { 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 { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js";
|
||||
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||
@@ -41,6 +43,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
let encrypt = this.plugin.settings.encrypt;
|
||||
let passphrase = this.plugin.settings.passphrase;
|
||||
let useDynamicIterationCount = this.plugin.settings.useDynamicIterationCount;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
@@ -291,68 +296,78 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
)
|
||||
|
||||
);
|
||||
new Setting(containerRemoteDatabaseEl)
|
||||
const e2e = new Setting(containerRemoteDatabaseEl)
|
||||
.setName("End to End Encryption")
|
||||
.setDesc("Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommend.")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => {
|
||||
toggle.setValue(encrypt).onChange(async (value) => {
|
||||
if (inWizard) {
|
||||
this.plugin.settings.encrypt = value;
|
||||
passphrase.setDisabled(!value);
|
||||
passphraseSetting.setDisabled(!value);
|
||||
dynamicIteration.setDisabled(!value);
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
this.plugin.settings.workingEncrypt = value;
|
||||
passphrase.setDisabled(!value);
|
||||
encrypt = value;
|
||||
passphraseSetting.setDisabled(!value);
|
||||
dynamicIteration.setDisabled(!value);
|
||||
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")
|
||||
.setDesc("Encrypting passphrase. If you change the passphrase of a existing database, overwriting the remote database is strongly recommended.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.workingPassphrase)
|
||||
.setValue(passphrase)
|
||||
.onChange(async (value) => {
|
||||
if (inWizard) {
|
||||
this.plugin.settings.passphrase = value;
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
this.plugin.settings.workingPassphrase = value;
|
||||
passphrase = value;
|
||||
await this.plugin.saveSettings();
|
||||
markDirtyControl();
|
||||
}
|
||||
});
|
||||
text.inputEl.setAttribute("type", "password");
|
||||
});
|
||||
passphrase.setDisabled(!this.plugin.settings.workingEncrypt);
|
||||
passphraseSetting.setDisabled(!encrypt);
|
||||
|
||||
const dynamicIteration = new Setting(containerRemoteDatabaseEl)
|
||||
.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)")
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.workingUseDynamicIterationCount)
|
||||
toggle.setValue(useDynamicIterationCount)
|
||||
.onChange(async (value) => {
|
||||
if (inWizard) {
|
||||
this.plugin.settings.useDynamicIterationCount = value;
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
this.plugin.settings.workingUseDynamicIterationCount = value;
|
||||
useDynamicIterationCount = value;
|
||||
await this.plugin.saveSettings();
|
||||
markDirtyControl();
|
||||
}
|
||||
});
|
||||
})
|
||||
.setClass("wizardHidden");
|
||||
dynamicIteration.setDisabled(!this.plugin.settings.workingEncrypt);
|
||||
dynamicIteration.setDisabled(!encrypt);
|
||||
|
||||
const checkWorkingPassphrase = async (): Promise<boolean> => {
|
||||
const settingForCheck: RemoteDBSettings = {
|
||||
...this.plugin.settings,
|
||||
encrypt: this.plugin.settings.workingEncrypt,
|
||||
passphrase: this.plugin.settings.workingPassphrase,
|
||||
useDynamicIterationCount: this.plugin.settings.workingUseDynamicIterationCount,
|
||||
encrypt: encrypt,
|
||||
passphrase: passphrase,
|
||||
useDynamicIterationCount: useDynamicIterationCount,
|
||||
};
|
||||
console.dir(settingForCheck);
|
||||
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) => {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (this.plugin.settings.workingEncrypt && !(await testCrypt())) {
|
||||
if (encrypt && !(await testCrypt())) {
|
||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
if (!(await checkWorkingPassphrase()) && !sendToServer) {
|
||||
return;
|
||||
}
|
||||
if (!this.plugin.settings.workingEncrypt) {
|
||||
this.plugin.settings.workingPassphrase = "";
|
||||
if (!encrypt) {
|
||||
passphrase = "";
|
||||
}
|
||||
this.plugin.settings.liveSync = false;
|
||||
this.plugin.settings.periodicReplication = false;
|
||||
@@ -390,11 +405,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.syncOnStart = false;
|
||||
this.plugin.settings.syncOnFileOpen = false;
|
||||
this.plugin.settings.syncAfterMerge = false;
|
||||
this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt;
|
||||
this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase;
|
||||
this.plugin.settings.useDynamicIterationCount = this.plugin.settings.workingUseDynamicIterationCount;
|
||||
this.plugin.settings.encrypt = encrypt;
|
||||
this.plugin.settings.passphrase = passphrase;
|
||||
this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount;
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
markDirtyControl();
|
||||
if (sendToServer) {
|
||||
await this.plugin.initializeDatabase(true);
|
||||
await this.plugin.markRemoteLocked();
|
||||
@@ -1184,8 +1200,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.customChunkSize + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 100) {
|
||||
v = 100;
|
||||
if (isNaN(v) || v < 1) {
|
||||
v = 1;
|
||||
}
|
||||
this.plugin.settings.customChunkSize = v;
|
||||
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` });
|
||||
infoApply.addClass("op-warn-info");
|
||||
infoApply.addClass("wizardOnly")
|
||||
@@ -1365,7 +1420,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : "self-hosted";
|
||||
pluginConfig.couchDB_USER = REDACTED;
|
||||
pluginConfig.passphrase = REDACTED;
|
||||
pluginConfig.workingPassphrase = REDACTED;
|
||||
pluginConfig.encryptedPassphrase = REDACTED;
|
||||
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
||||
|
||||
const msgConfig = `----remote config----
|
||||
${stringifyYaml(responseConfig)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { onMount } from "svelte";
|
||||
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";
|
||||
|
||||
|
||||
@@ -52,14 +52,14 @@ export class InputStringDialog extends Modal {
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl("h1", { text: this.title });
|
||||
|
||||
new Setting(contentEl).setName(this.key).addText((text) =>
|
||||
// For enter to submit
|
||||
const formEl = contentEl.createEl("form");
|
||||
new Setting(formEl).setName(this.key).addText((text) =>
|
||||
text.onChange((value) => {
|
||||
this.result = value;
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(contentEl).addButton((btn) =>
|
||||
new Setting(formEl).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Ok")
|
||||
.setCta()
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 2284678a59...14fecd2411
603
src/main.ts
603
src/main.ts
@@ -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 { 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 } from "./types";
|
||||
import {
|
||||
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 { 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 { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo, queueItem, FileInfo } from "./types";
|
||||
import { getDocData, isDocContentSame } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { LocalPouchDB } from "./LocalPouchDB";
|
||||
import { LogDisplayModal } from "./LogDisplayModal";
|
||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
|
||||
|
||||
|
||||
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;
|
||||
|
||||
import { InputStringDialog, PluginDialogModal, PopoverSelectString } from "./dialogs";
|
||||
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);
|
||||
|
||||
@@ -48,6 +33,7 @@ const FileWatchEventQueueMax = 10;
|
||||
|
||||
function getAbstractFileByPath(path: string): TAbstractFile | null {
|
||||
// Hidden API but so useful.
|
||||
// @ts-ignore
|
||||
if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
|
||||
// @ts-ignore
|
||||
return app.vault.getAbstractFileByPathInsensitive(path);
|
||||
@@ -126,19 +112,20 @@ function clearTouched() {
|
||||
type CacheData = string | ArrayBuffer;
|
||||
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
||||
type FileEventArgs = {
|
||||
file: TAbstractFile | InternalFileInfo;
|
||||
file: FileInfo | InternalFileInfo;
|
||||
cache?: CacheData;
|
||||
oldPath?: string;
|
||||
ctx?: any;
|
||||
}
|
||||
type FileEventItem = {
|
||||
type: FileEventType,
|
||||
args: FileEventArgs
|
||||
args: FileEventArgs,
|
||||
key: string,
|
||||
}
|
||||
|
||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
settings: ObsidianLiveSyncSettings;
|
||||
localDatabase: LocalPouchDB;
|
||||
logMessage: string[] = [];
|
||||
statusBar: HTMLElement;
|
||||
statusBar2: HTMLElement;
|
||||
suspended: boolean;
|
||||
@@ -285,7 +272,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
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");
|
||||
//@ts-ignore
|
||||
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
|
||||
@@ -345,7 +332,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
this.statusBar = this.addStatusBarItem();
|
||||
this.statusBar.addClass("syncstatusbar");
|
||||
this.refreshStatusText = this.refreshStatusText.bind(this);
|
||||
|
||||
this.statusBar2 = this.addStatusBarItem();
|
||||
this.watchVaultChange = this.watchVaultChange.bind(this);
|
||||
@@ -360,6 +346,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
||||
|
||||
this.setPeriodicSync = this.setPeriodicSync.bind(this);
|
||||
this.clearPeriodicSync = this.clearPeriodicSync.bind(this);
|
||||
this.periodicSync = this.periodicSync.bind(this);
|
||||
this.loadQueuedFiles = this.loadQueuedFiles.bind(this);
|
||||
|
||||
@@ -411,11 +398,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const configURIBase = "obsidian://setuplivesync?settings=";
|
||||
this.addCommand({
|
||||
id: "livesync-copysetupuri",
|
||||
name: "Copy setup URI",
|
||||
name: "Copy the setup URI",
|
||||
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;
|
||||
const setting = { ...this.settings };
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
|
||||
for (const k of keys) {
|
||||
if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) {
|
||||
@@ -430,11 +417,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-copysetupurifull",
|
||||
name: "Copy setup URI (Full)",
|
||||
name: "Copy the setup URI (Full)",
|
||||
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;
|
||||
const setting = { ...this.settings };
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false));
|
||||
const uri = `${configURIBase}${encryptedSetting}`;
|
||||
await navigator.clipboard.writeText(uri);
|
||||
@@ -443,7 +430,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-opensetupuri",
|
||||
name: "Open setup URI",
|
||||
name: "Open the setup URI",
|
||||
callback: async () => {
|
||||
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
|
||||
if (setupURI === false) return;
|
||||
@@ -459,16 +446,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const setupWizard = async (confString: string) => {
|
||||
try {
|
||||
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;
|
||||
const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false));
|
||||
if (newConf) {
|
||||
const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?");
|
||||
if (result == "yes") {
|
||||
const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf);
|
||||
const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf) as ObsidianLiveSyncSettings;
|
||||
this.localDatabase.closeReplication();
|
||||
this.settings.suspendFileWatching = true;
|
||||
console.dir(newSettingW);
|
||||
// Back into the default method once.
|
||||
newSettingW.configPassphraseStore = "";
|
||||
newSettingW.encryptedPassphrase = "";
|
||||
newSettingW.encryptedCouchDBConnection = "";
|
||||
const setupJustImport = "Just import setting";
|
||||
const setupAsNew = "Set it up as secondary or subsequent device";
|
||||
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]);
|
||||
if (setupType == setupJustImport) {
|
||||
this.settings = newSettingW;
|
||||
this.usedPassphrase = "";
|
||||
await this.saveSettings();
|
||||
} else if (setupType == setupAsNew) {
|
||||
this.settings = newSettingW;
|
||||
this.usedPassphrase = "";
|
||||
await this.saveSettings();
|
||||
await this.resetLocalOldDatabase();
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
this.settings = newSettingW;
|
||||
this.usedPassphrase = "";
|
||||
await this.saveSettings();
|
||||
await this.resetLocalOldDatabase();
|
||||
await this.resetLocalDatabase();
|
||||
@@ -507,6 +502,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (keepLocalDB == "yes" && keepRemoteDB == "yes") {
|
||||
// nothing to do. so peaceful.
|
||||
this.settings = newSettingW;
|
||||
this.usedPassphrase = "";
|
||||
await this.saveSettings();
|
||||
const replicate = await askYesNo(this.app, "Unlock and replicate?");
|
||||
if (replicate == "yes") {
|
||||
@@ -526,6 +522,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
let initDB;
|
||||
this.settings = newSettingW;
|
||||
this.usedPassphrase = "";
|
||||
await this.saveSettings();
|
||||
if (keepLocalDB == "no") {
|
||||
this.resetLocalOldDatabase();
|
||||
@@ -634,9 +631,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000);
|
||||
this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000);
|
||||
setLockNotifier(() => {
|
||||
this.refreshStatusText();
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-plugin-dialog",
|
||||
name: "Show Plugins and their settings",
|
||||
@@ -726,17 +721,97 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
//@ts-ignore
|
||||
const isMobile = this.app.isMobile;
|
||||
this.localDatabase = new LocalPouchDB(this.settings, vaultName, isMobile);
|
||||
this.localDatabase.updateInfo = () => {
|
||||
this.refreshStatusText();
|
||||
};
|
||||
this.observeForLogs();
|
||||
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() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
this.settings.workingEncrypt = this.settings.encrypt;
|
||||
this.settings.workingPassphrase = this.settings.passphrase;
|
||||
const settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) as ObsidianLiveSyncSettings;
|
||||
const passphrase = await this.getPassphrase(settings);
|
||||
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.
|
||||
this.settings.disableRequestURI = true;
|
||||
|
||||
@@ -768,7 +843,29 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
||||
|
||||
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.triggerRealizeSettingSyncMode();
|
||||
}
|
||||
@@ -833,58 +930,79 @@ 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;
|
||||
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
||||
let forcePerform = false;
|
||||
for (const param of params) {
|
||||
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;
|
||||
// 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) {
|
||||
let cache: null | string | ArrayBuffer;
|
||||
// new file or something changed, cache the changes.
|
||||
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
|
||||
if (recentlyTouched(file)) {
|
||||
continue;
|
||||
}
|
||||
if (this.watchedFileEventQueue[i].type != type) break;
|
||||
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
||||
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 (type == "DELETE" || type == "RENAME") {
|
||||
forcePerform = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.watchedFileEventQueue.push({
|
||||
type,
|
||||
args: {
|
||||
file,
|
||||
oldPath,
|
||||
cache,
|
||||
ctx
|
||||
|
||||
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;
|
||||
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 });
|
||||
console.dir([...this.watchedFileEventQueue]);
|
||||
if (this.isReady) {
|
||||
await this.procFileEvent();
|
||||
await this.procFileEvent(forcePerform);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -902,62 +1020,76 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
clearTrigger("applyBatchAuto");
|
||||
const ret = await runWithLock("procFiles", true, async () => {
|
||||
L2:
|
||||
do {
|
||||
const procs = [...this.watchedFileEventQueue];
|
||||
this.watchedFileEventQueue = [];
|
||||
for (const queue of procs) {
|
||||
|
||||
L1:
|
||||
do {
|
||||
const queue = procs.shift();
|
||||
if (queue === undefined) break L1;
|
||||
console.warn([queue.type, { ...queue.args, cache: undefined }]);
|
||||
|
||||
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.deleteFromDBbyPath(file.path);
|
||||
} else if (queue.type == "INTERNAL") {
|
||||
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.stat.mtime);
|
||||
}
|
||||
}
|
||||
this.refreshStatusText();
|
||||
await this.localDatabase.kvDB.set(key, file.mtime);
|
||||
} while (procs.length > 0);
|
||||
} while (this.watchedFileEventQueue.length != 0);
|
||||
return true;
|
||||
})
|
||||
this.refreshStatusText();
|
||||
return ret;
|
||||
}
|
||||
|
||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent("CREATE", file, null, ctx);
|
||||
this.appendWatchEvent([{ type: "CREATE", file }], ctx);
|
||||
}
|
||||
|
||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent("CHANGED", file, null, ctx);
|
||||
this.appendWatchEvent([{ type: "CHANGED", file }], ctx);
|
||||
}
|
||||
|
||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent("DELETE", file, null, ctx);
|
||||
this.appendWatchEvent([{ type: "DELETE", file }], ctx);
|
||||
}
|
||||
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) {
|
||||
@@ -994,11 +1126,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
.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);
|
||||
this.appendWatchEvent(
|
||||
[{
|
||||
type: "INTERNAL",
|
||||
file: { 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;
|
||||
@@ -1056,38 +1191,23 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
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);
|
||||
if (file instanceof TFolder) {
|
||||
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) {
|
||||
if (file instanceof TFile) {
|
||||
try {
|
||||
Logger(`file save ${file.path} into db`);
|
||||
await this.updateIntoDB(file, false, cache);
|
||||
Logger(`deleted ${oldFile} from db`);
|
||||
await this.deleteFromDBbyPath(oldFile);
|
||||
// Logger(`RENAMING.. ${file.path} into db`);
|
||||
if (await this.updateIntoDB(file, false, cache)) {
|
||||
// Logger(`deleted ${oldFile} from db`);
|
||||
await this.deleteFromDBbyPath(oldFile);
|
||||
} else {
|
||||
Logger(`Could not save new file: ${file.path} `, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addLogHook: () => void = null;
|
||||
//--> Basic document Functions
|
||||
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
|
||||
|
||||
@@ -1108,12 +1228,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 newMessage = timestamp + "->" + messageContent;
|
||||
|
||||
this.logMessage = [].concat(this.logMessage).concat([newMessage]).slice(-100);
|
||||
console.log(vaultName + ":" + newMessage);
|
||||
logMessageStore.apply(e => [...e, newMessage].slice(-100));
|
||||
this.setStatusBarText(null, messageContent.substring(0, 30));
|
||||
// if (message instanceof Error) {
|
||||
// console.trace(message);
|
||||
// }
|
||||
|
||||
if (level >= LOG_LEVEL.NOTICE) {
|
||||
if (!key) key = messageContent;
|
||||
@@ -1152,7 +1269,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
};
|
||||
}
|
||||
}
|
||||
if (this.addLogHook != null) this.addLogHook();
|
||||
}
|
||||
|
||||
async ensureDirectory(fullPath: string) {
|
||||
@@ -1200,7 +1316,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
Logger(msg + path);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
@@ -1220,7 +1335,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
Logger(msg + path);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
@@ -1243,11 +1357,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
} else {
|
||||
await this.app.vault.delete(file);
|
||||
}
|
||||
Logger(`deleted:${file.path}`);
|
||||
Logger(`other items:${dir.children.length}`);
|
||||
Logger(`xxx <- STORAGE (deleted) ${file.path}`);
|
||||
Logger(`files: ${dir.children.length}`);
|
||||
if (dir.children.length == 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1355,7 +1469,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
);
|
||||
this.refreshStatusText();
|
||||
}
|
||||
async handleDBChangedAsync(change: EntryBody) {
|
||||
|
||||
@@ -1400,13 +1513,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
queuedFiles: {
|
||||
entry: EntryBody;
|
||||
missingChildren: string[];
|
||||
timeout?: number;
|
||||
done?: boolean;
|
||||
warned?: boolean;
|
||||
}[] = [];
|
||||
queuedFiles = [] as queueItem[];
|
||||
queuedFilesStore = getGlobalStore("queuedFiles", { queuedItems: [] as queueItem[], fileEventItems: [] as FileEventItem[] });
|
||||
chunkWaitTimeout = 60000;
|
||||
|
||||
saveQueuedFiles() {
|
||||
@@ -1433,7 +1541,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await this.syncInternalFilesAndDatabase("pull", false, false, w);
|
||||
Logger(`Applying hidden ${w.length} files changed`);
|
||||
});
|
||||
this.refreshStatusText();
|
||||
}
|
||||
procInternalFile(filename: string) {
|
||||
this.procInternalFiles.push(filename);
|
||||
@@ -1465,6 +1572,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
this.queuedFiles = this.queuedFiles.filter((e) => !e.done);
|
||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||
this.saveQueuedFiles();
|
||||
}
|
||||
parseIncomingChunk(chunk: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||
@@ -1528,7 +1636,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
//---> Sync
|
||||
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
|
||||
this.refreshStatusText();
|
||||
for (const change of docs) {
|
||||
if (isPluginChunk(change._id)) {
|
||||
if (this.settings.notifyPluginOrSettingUpdated) {
|
||||
@@ -1600,8 +1707,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
setPeriodicSync() {
|
||||
this.clearPeriodicSync();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1643,7 +1750,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
if (this.settings.liveSync) {
|
||||
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
||||
this.refreshStatusText();
|
||||
}
|
||||
if (this.settings.syncInternalFiles) {
|
||||
await this.syncInternalFilesAndDatabase("safe", false);
|
||||
@@ -1655,63 +1761,80 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
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() {
|
||||
const sent = this.localDatabase.docSent;
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
logHideTimer: NodeJS.Timeout = null;
|
||||
@@ -1726,10 +1849,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const root = activeDocument.documentElement;
|
||||
const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`);
|
||||
q.forEach(e => e.setAttr("data-log", '' + (newMsg + "\n" + newLog) + ''))
|
||||
// root.style.setProperty("--slsmessage", '"' + (newMsg + "\n" + newLog).split("\n").join("\\a ") + '"');
|
||||
} else {
|
||||
const root = activeDocument.documentElement;
|
||||
// root.style.setProperty("--slsmessage", '""');
|
||||
const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`);
|
||||
q.forEach(e => e.setAttr("data-log", ''))
|
||||
}
|
||||
@@ -2278,7 +2399,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
showMergeDialog(filename: string, conflictCheckResult: diff_result): Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
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);
|
||||
if (testDoc === false) {
|
||||
Logger("Missing file..", LOG_LEVEL.VERBOSE);
|
||||
@@ -2460,9 +2581,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
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)) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
let content: string | string[];
|
||||
let datatype: "plain" | "newnote" = "newnote";
|
||||
@@ -2524,15 +2645,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (isNotChanged) return;
|
||||
await this.localDatabase.putDBEntry(d, initialScan);
|
||||
if (isNotChanged) return true;
|
||||
const ret = await this.localDatabase.putDBEntry(d, initialScan);
|
||||
this.queuedFiles = this.queuedFiles.map((e) => ({ ...e, ...(e.entry._id == d._id ? { done: true } : {}) }));
|
||||
|
||||
|
||||
Logger(msg + fullPath);
|
||||
if (this.settings.syncOnSave && !this.suspended) {
|
||||
await this.replicate();
|
||||
}
|
||||
return ret != false;
|
||||
}
|
||||
|
||||
async deleteFromDB(file: TFile) {
|
||||
|
||||
21
src/types.ts
21
src/types.ts
@@ -1,5 +1,5 @@
|
||||
import { PluginManifest } from "obsidian";
|
||||
import { DatabaseEntry } from "./lib/src/types";
|
||||
import { PluginManifest, TFile } from "obsidian";
|
||||
import { DatabaseEntry, EntryBody } from "./lib/src/types";
|
||||
|
||||
export interface PluginDataEntry extends DatabaseEntry {
|
||||
deviceVaultName: string;
|
||||
@@ -30,3 +30,20 @@ export interface InternalFileInfo {
|
||||
size: number;
|
||||
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;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
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.
|
||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
|
||||
@@ -247,4 +247,8 @@ div.sls-setting-menu-btn {
|
||||
|
||||
.sls-setting:not(.isWizard) .wizardOnly {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sls-item-dirty::before {
|
||||
content: "✏";
|
||||
}
|
||||
58
updates.md
58
updates.md
@@ -35,49 +35,37 @@
|
||||
- Fixed button styling.
|
||||
- Changed:
|
||||
- Conflict checking on synchronising has been enabled for every note in default.
|
||||
|
||||
- 0.17.8
|
||||
- Improved: Performance improved. Prebuilt PouchDB is no longer used.
|
||||
- Fixed: Merging hidden files is also fixed.
|
||||
- New Feature: Now we can synchronise automatically after merging conflicts.
|
||||
|
||||
- 0.17.9
|
||||
- Fixed: Conflict merge of internal files is no longer broken.
|
||||
- Improved: Smoother status display inside the editor.
|
||||
|
||||
- 0.17.10
|
||||
- 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.
|
||||
### 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.
|
||||
- 0.17.11
|
||||
- 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.
|
||||
- 0.17.12: Skipped.
|
||||
- 0.17.13
|
||||
- Fixed: Document history is now displayed again.
|
||||
- Reorganised: Many files have been refactored.
|
||||
- 0.17.14
|
||||
- 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.
|
||||
|
||||
And, minor changes have been included.
|
||||
|
||||
#### 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.
|
||||
... To continue on to `updates_old.md`.
|
||||
@@ -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
|
||||
- The target selecting filter was implemented.
|
||||
Now we can set what files are synchronised by regular expression.
|
||||
|
||||
Reference in New Issue
Block a user