mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-22 22:21:29 +00:00
Fixed:
- Fixed problems about saving or deleting files to the local database. - Disable version up warning. - Fixed error on folder renaming. - Merge dialog is now shown one by one. - Fixed icons of queued files. - Handled sync issue of Folder to File - Fixed the messages in the setting dialog. - Fixed deadlock.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"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
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"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",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export class ConflictResolveModal extends Modal {
|
|||||||
// result: Array<[number, string]>;
|
// result: Array<[number, string]>;
|
||||||
result: diff_result;
|
result: diff_result;
|
||||||
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, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
|
||||||
super(app);
|
super(app);
|
||||||
this.result = diff;
|
this.result = diff;
|
||||||
@@ -45,18 +46,21 @@ export class ConflictResolveModal extends Modal {
|
|||||||
contentEl.createEl("button", { text: "Keep A" }, (e) => {
|
contentEl.createEl("button", { text: "Keep A" }, (e) => {
|
||||||
e.addEventListener("click", async () => {
|
e.addEventListener("click", async () => {
|
||||||
await this.callback(this.result.right.rev);
|
await this.callback(this.result.right.rev);
|
||||||
|
this.callback = null;
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
contentEl.createEl("button", { text: "Keep B" }, (e) => {
|
contentEl.createEl("button", { text: "Keep B" }, (e) => {
|
||||||
e.addEventListener("click", async () => {
|
e.addEventListener("click", async () => {
|
||||||
await this.callback(this.result.left.rev);
|
await this.callback(this.result.left.rev);
|
||||||
|
this.callback = null;
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
contentEl.createEl("button", { text: "Concat both" }, (e) => {
|
contentEl.createEl("button", { text: "Concat both" }, (e) => {
|
||||||
e.addEventListener("click", async () => {
|
e.addEventListener("click", async () => {
|
||||||
await this.callback(null);
|
await this.callback("");
|
||||||
|
this.callback = null;
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -70,5 +74,8 @@ export class ConflictResolveModal extends Modal {
|
|||||||
onClose() {
|
onClose() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
|
if (this.callback != null) {
|
||||||
|
this.callback(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,8 +403,10 @@ export class LocalPouchDB {
|
|||||||
async deleteDBEntry(path: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> {
|
async deleteDBEntry(path: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> {
|
||||||
await this.waitForGCComplete();
|
await this.waitForGCComplete();
|
||||||
const id = path2id(path);
|
const id = path2id(path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let obj: EntryDocResponse = null;
|
let obj: EntryDocResponse = null;
|
||||||
|
return await runWithLock("file:" + id, false, async () => {
|
||||||
if (opt) {
|
if (opt) {
|
||||||
obj = await this.localDatabase.get(id, opt);
|
obj = await this.localDatabase.get(id, opt);
|
||||||
} else {
|
} else {
|
||||||
@@ -438,6 +440,7 @@ export class LocalPouchDB {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (ex.status && ex.status == 404) {
|
if (ex.status && ex.status == 404) {
|
||||||
return false;
|
return false;
|
||||||
@@ -478,10 +481,13 @@ export class LocalPouchDB {
|
|||||||
let notfound = 0;
|
let notfound = 0;
|
||||||
for (const v of delDocs) {
|
for (const v of delDocs) {
|
||||||
try {
|
try {
|
||||||
|
await runWithLock("file:" + v, false, async () => {
|
||||||
const item = await this.localDatabase.get(v);
|
const item = await this.localDatabase.get(v);
|
||||||
item._deleted = true;
|
item._deleted = true;
|
||||||
await this.localDatabase.put(item);
|
await this.localDatabase.put(item);
|
||||||
this.updateRecentModifiedDocs(item._id, item._rev, true);
|
this.updateRecentModifiedDocs(item._id, item._rev, true);
|
||||||
|
});
|
||||||
|
|
||||||
deleteCount++;
|
deleteCount++;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (ex.status && ex.status == 404) {
|
if (ex.status && ex.status == 404) {
|
||||||
@@ -540,7 +546,6 @@ export class LocalPouchDB {
|
|||||||
cPieceSize = 0;
|
cPieceSize = 0;
|
||||||
// lookup for next splittion .
|
// lookup for next splittion .
|
||||||
// we're standing on "\n"
|
// we're standing on "\n"
|
||||||
// debugger
|
|
||||||
do {
|
do {
|
||||||
const n1 = leftData.indexOf("\n", cPieceSize + 1);
|
const n1 = leftData.indexOf("\n", cPieceSize + 1);
|
||||||
const n2 = leftData.indexOf("\n\n", cPieceSize + 1);
|
const n2 = leftData.indexOf("\n\n", cPieceSize + 1);
|
||||||
@@ -691,6 +696,7 @@ export class LocalPouchDB {
|
|||||||
type: plainSplit ? "plain" : "newnote",
|
type: plainSplit ? "plain" : "newnote",
|
||||||
};
|
};
|
||||||
// Here for upsert logic,
|
// Here for upsert logic,
|
||||||
|
await runWithLock("file:" + newDoc._id, false, async () => {
|
||||||
try {
|
try {
|
||||||
const old = await this.localDatabase.get(newDoc._id);
|
const old = await this.localDatabase.get(newDoc._id);
|
||||||
if (!old.type || old.type == "notes" || old.type == "newnote" || old.type == "plain") {
|
if (!old.type || old.type == "notes" || old.type == "newnote" || old.type == "plain") {
|
||||||
@@ -718,6 +724,7 @@ export class LocalPouchDB {
|
|||||||
} else {
|
} else {
|
||||||
Logger(`note saved:${newDoc._id}:${r.rev}`);
|
Logger(`note saved:${newDoc._id}:${r.rev}`);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
Logger(`note coud not saved:${note._id}`);
|
Logger(`note coud not saved:${note._id}`);
|
||||||
}
|
}
|
||||||
@@ -837,6 +844,15 @@ export class LocalPouchDB {
|
|||||||
locked: false,
|
locked: false,
|
||||||
accepted_nodes: [this.nodeid],
|
accepted_nodes: [this.nodeid],
|
||||||
};
|
};
|
||||||
|
// const remoteInfo = dbret.info;
|
||||||
|
// const localInfo = await this.localDatabase.info();
|
||||||
|
// const remoteDocsCount = remoteInfo.doc_count;
|
||||||
|
// const localDocsCount = localInfo.doc_count;
|
||||||
|
// const remoteUpdSeq = typeof remoteInfo.update_seq == "string" ? Number(remoteInfo.update_seq.split("-")[0]) : remoteInfo.update_seq;
|
||||||
|
// const localUpdSeq = typeof localInfo.update_seq == "string" ? Number(localInfo.update_seq.split("-")[0]) : localInfo.update_seq;
|
||||||
|
|
||||||
|
// Logger(`Database diffences: remote:${remoteDocsCount} docs / last update ${remoteUpdSeq}`);
|
||||||
|
// Logger(`Database diffences: local :${localDocsCount} docs / last update ${localUpdSeq}`);
|
||||||
|
|
||||||
const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint);
|
const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint);
|
||||||
this.remoteLocked = remoteMilestone.locked;
|
this.remoteLocked = remoteMilestone.locked;
|
||||||
@@ -856,7 +872,7 @@ export class LocalPouchDB {
|
|||||||
};
|
};
|
||||||
const syncOption: PouchDB.Replication.SyncOptions = keepAlive ? { live: true, retry: true, heartbeat: 30000, ...syncOptionBase } : { ...syncOptionBase };
|
const syncOption: PouchDB.Replication.SyncOptions = keepAlive ? { live: true, retry: true, heartbeat: 30000, ...syncOptionBase } : { ...syncOptionBase };
|
||||||
|
|
||||||
return { db: dbret.db, syncOptionBase, syncOption };
|
return { db: dbret.db, info: dbret.info, syncOptionBase, syncOption };
|
||||||
}
|
}
|
||||||
|
|
||||||
async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>): Promise<boolean> {
|
async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>): Promise<boolean> {
|
||||||
@@ -996,7 +1012,6 @@ export class LocalPouchDB {
|
|||||||
this.cancelHandler(replicate);
|
this.cancelHandler(replicate);
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
if (notice != null) notice.hide();
|
if (notice != null) notice.hide();
|
||||||
// debugger;
|
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const containerRemoteDatabaseEl = containerEl.createDiv();
|
const containerRemoteDatabaseEl = containerEl.createDiv();
|
||||||
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
|
containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" });
|
||||||
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: "The remote configuration is locked while any synchronization is enabled." });
|
const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: `These settings are kept locked while automatic synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` });
|
||||||
syncWarn.addClass("op-warn");
|
syncWarn.addClass("op-warn");
|
||||||
syncWarn.addClass("sls-hidden");
|
syncWarn.addClass("sls-hidden");
|
||||||
|
|
||||||
|
|||||||
194
src/main.ts
194
src/main.ts
@@ -1,7 +1,7 @@
|
|||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "obsidian";
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "obsidian";
|
||||||
import { diff_match_patch } from "diff-match-patch";
|
import { diff_match_patch } from "diff-match-patch";
|
||||||
|
|
||||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, PluginDataEntry, LOG_LEVEL, VER, PERIODIC_PLUGIN_SWEEP, DEFAULT_SETTINGS, PluginList, DevicePluginList } from "./types";
|
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, PluginDataEntry, LOG_LEVEL, VER, PERIODIC_PLUGIN_SWEEP, DEFAULT_SETTINGS, PluginList, DevicePluginList, diff_result } from "./types";
|
||||||
import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock } from "./utils";
|
import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock } from "./utils";
|
||||||
import { Logger, setLogger } from "./logger";
|
import { Logger, setLogger } from "./logger";
|
||||||
import { LocalPouchDB } from "./LocalPouchDB";
|
import { LocalPouchDB } from "./LocalPouchDB";
|
||||||
@@ -28,7 +28,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName();
|
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName();
|
||||||
const last_version = localStorage.getItem(lsname);
|
const last_version = localStorage.getItem(lsname);
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
if (!last_version || Number(last_version) < VER) {
|
if (last_version && Number(last_version) < VER) {
|
||||||
this.settings.liveSync = false;
|
this.settings.liveSync = false;
|
||||||
this.settings.syncOnSave = false;
|
this.settings.syncOnSave = false;
|
||||||
this.settings.syncOnStart = false;
|
this.settings.syncOnStart = false;
|
||||||
@@ -116,6 +116,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
|
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.addCommand({
|
||||||
|
id: "livesync-checkdoc-conflicted",
|
||||||
|
name: "Resolve if conflicted.",
|
||||||
|
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
||||||
|
await this.showIfConflicted(view.file);
|
||||||
|
},
|
||||||
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-gc",
|
id: "livesync-gc",
|
||||||
name: "garbage collect now",
|
name: "garbage collect now",
|
||||||
@@ -284,19 +291,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.watchVaultChangeAsync(file, ...args);
|
this.watchVaultChangeAsync(file, ...args);
|
||||||
}
|
}
|
||||||
async applyBatchChange() {
|
async applyBatchChange() {
|
||||||
|
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return await runWithLock("batchSave", false, async () => {
|
return await runWithLock("batchSave", false, async () => {
|
||||||
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
||||||
this.batchFileChange = [];
|
this.batchFileChange = [];
|
||||||
const files = this.app.vault.getFiles();
|
|
||||||
const promises = batchItems.map(async (e) => {
|
const promises = batchItems.map(async (e) => {
|
||||||
try {
|
try {
|
||||||
if (await this.app.vault.adapter.exists(normalizePath(e))) {
|
const f = this.app.vault.getAbstractFileByPath(normalizePath(e));
|
||||||
const f = files.find((f) => f.path == e);
|
if (f && f instanceof TFile) {
|
||||||
if (f) {
|
|
||||||
await this.updateIntoDB(f);
|
await this.updateIntoDB(f);
|
||||||
Logger(`Batch save:${e}`);
|
Logger(`Batch save:${e}`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE);
|
Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE);
|
||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
@@ -358,25 +365,37 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any) {
|
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any) {
|
||||||
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
|
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||||
|
try {
|
||||||
await this.applyBatchChange();
|
await this.applyBatchChange();
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(ex);
|
||||||
|
}
|
||||||
if (file instanceof TFolder) {
|
if (file instanceof TFolder) {
|
||||||
const newFiles = this.GetAllFilesRecursively(file);
|
const newFiles = this.GetAllFilesRecursively(file);
|
||||||
// for guard edge cases. this won't happen and each file's event will be raise.
|
// for guard edge cases. this won't happen and each file's event will be raise.
|
||||||
for (const i of newFiles) {
|
for (const i of newFiles) {
|
||||||
|
try {
|
||||||
const newFilePath = normalizePath(this.getFilePath(i));
|
const newFilePath = normalizePath(this.getFilePath(i));
|
||||||
const newFile = this.app.vault.getAbstractFileByPath(newFilePath);
|
const newFile = this.app.vault.getAbstractFileByPath(newFilePath);
|
||||||
if (newFile instanceof TFile) {
|
if (newFile instanceof TFile) {
|
||||||
Logger(`save ${newFile.path} into db`);
|
Logger(`save ${newFile.path} into db`);
|
||||||
await this.updateIntoDB(newFile);
|
await this.updateIntoDB(newFile);
|
||||||
}
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Logger(`delete below ${oldFile} from db`);
|
Logger(`delete below ${oldFile} from db`);
|
||||||
await this.deleteFromDBbyPath(oldFile);
|
await this.deleteFromDBbyPath(oldFile);
|
||||||
} else if (file instanceof TFile) {
|
} else if (file instanceof TFile) {
|
||||||
|
try {
|
||||||
Logger(`file save ${file.path} into db`);
|
Logger(`file save ${file.path} into db`);
|
||||||
await this.updateIntoDB(file);
|
await this.updateIntoDB(file);
|
||||||
Logger(`deleted ${oldFile} into db`);
|
Logger(`deleted ${oldFile} into db`);
|
||||||
await this.deleteFromDBbyPath(oldFile);
|
await this.deleteFromDBbyPath(oldFile);
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.gcHook();
|
this.gcHook();
|
||||||
}
|
}
|
||||||
@@ -398,9 +417,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
|
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
|
||||||
console.log(valutName + ":" + newmessage);
|
console.log(valutName + ":" + newmessage);
|
||||||
// if (this.statusBar2 != null) {
|
|
||||||
// this.statusBar2.setText(newmessage.substring(0, 60));
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (level >= LOG_LEVEL.NOTICE) {
|
if (level >= LOG_LEVEL.NOTICE) {
|
||||||
if (messagecontent in this.notifies) {
|
if (messagecontent in this.notifies) {
|
||||||
@@ -468,7 +484,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
try {
|
try {
|
||||||
const newfile = await this.app.vault.createBinary(normalizePath(path), bin, { ctime: doc.ctime, mtime: doc.mtime });
|
const newfile = await this.app.vault.createBinary(normalizePath(path), bin, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger("live : write to local (newfile:b) " + path);
|
Logger("live : write to local (newfile:b) " + path);
|
||||||
await this.app.vault.trigger("create", newfile);
|
this.app.vault.trigger("create", newfile);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger("could not write to local (newfile:bin) " + path, LOG_LEVEL.NOTICE);
|
Logger("could not write to local (newfile:bin) " + path, LOG_LEVEL.NOTICE);
|
||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
@@ -483,7 +499,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
try {
|
try {
|
||||||
const newfile = await this.app.vault.create(normalizePath(path), doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
const newfile = await this.app.vault.create(normalizePath(path), doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger("live : write to local (newfile:p) " + path);
|
Logger("live : write to local (newfile:p) " + path);
|
||||||
await this.app.vault.trigger("create", newfile);
|
this.app.vault.trigger("create", newfile);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger("could not write to local (newfile:plain) " + path, LOG_LEVEL.NOTICE);
|
Logger("could not write to local (newfile:plain) " + path, LOG_LEVEL.NOTICE);
|
||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
@@ -544,7 +560,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
try {
|
try {
|
||||||
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger(msg);
|
Logger(msg);
|
||||||
await this.app.vault.trigger("modify", file);
|
this.app.vault.trigger("modify", file);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger("could not write to local (modify:bin) " + path, LOG_LEVEL.NOTICE);
|
Logger("could not write to local (modify:bin) " + path, LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
@@ -558,7 +574,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
try {
|
try {
|
||||||
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger(msg);
|
Logger(msg);
|
||||||
await this.app.vault.trigger("modify", file);
|
this.app.vault.trigger("modify", file);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger("could not write to local (modify:plain) " + path, LOG_LEVEL.NOTICE);
|
Logger("could not write to local (modify:plain) " + path, LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
@@ -574,20 +590,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async handleDBChanged(change: EntryBody) {
|
async handleDBChanged(change: EntryBody) {
|
||||||
const allfiles = this.app.vault.getFiles();
|
const targetFile = this.app.vault.getAbstractFileByPath(id2path(change._id));
|
||||||
const targetFiles = allfiles.filter((e) => e.path == id2path(change._id));
|
if (targetFile == null) {
|
||||||
if (targetFiles.length == 0) {
|
|
||||||
if (change._deleted) {
|
if (change._deleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const doc = change;
|
const doc = change;
|
||||||
await this.doc2storage_create(doc);
|
await this.doc2storage_create(doc);
|
||||||
}
|
} else if (targetFile instanceof TFile) {
|
||||||
if (targetFiles.length == 1) {
|
|
||||||
const doc = change;
|
const doc = change;
|
||||||
const file = targetFiles[0];
|
const file = targetFile;
|
||||||
await this.doc2storate_modify(doc, file);
|
await this.doc2storate_modify(doc, file);
|
||||||
await this.showIfConflicted(file);
|
this.queueConflictedCheck(file);
|
||||||
|
} else {
|
||||||
|
Logger(`${id2path(change._id)} is already exist as the folder`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,7 +736,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
let waiting = "";
|
let waiting = "";
|
||||||
if (this.settings.batchSave) {
|
if (this.settings.batchSave) {
|
||||||
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
|
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
|
||||||
waiting = waiting.replace(/🛫{10}/g, "🚀");
|
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
||||||
}
|
}
|
||||||
const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}`;
|
const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}`;
|
||||||
this.setStatusBarText(message);
|
this.setStatusBarText(message);
|
||||||
@@ -909,7 +925,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
// --> conflict resolving
|
// --> conflict resolving
|
||||||
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
|
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
|
||||||
try {
|
try {
|
||||||
const doc = await this.localDatabase.getDBEntry(path, { rev: rev });
|
const doc = await this.localDatabase.getDBEntry(path, { rev: rev }, false, false);
|
||||||
if (doc === false) return false;
|
if (doc === false) return false;
|
||||||
let data = doc.data;
|
let data = doc.data;
|
||||||
if (doc.datatype == "newnote") {
|
if (doc.datatype == "newnote") {
|
||||||
@@ -936,7 +952,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
* @returns true -> resolved, false -> nothing to do, or check result.
|
* @returns true -> resolved, false -> nothing to do, or check result.
|
||||||
*/
|
*/
|
||||||
async getConflictedStatus(path: string): Promise<diff_check_result> {
|
async getConflictedStatus(path: string): Promise<diff_check_result> {
|
||||||
const test = await this.localDatabase.getDBEntry(path, { conflicts: true });
|
const test = await this.localDatabase.getDBEntry(path, { conflicts: true }, false, false);
|
||||||
if (test === false) return false;
|
if (test === false) return false;
|
||||||
if (test == null) return false;
|
if (test == null) return false;
|
||||||
if (!test._conflicts) return false;
|
if (!test._conflicts) return false;
|
||||||
@@ -990,69 +1006,110 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
diff: diff,
|
diff: diff,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
showMergeDialog(file: TFile, conflictCheckResult: diff_result): Promise<boolean> {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
|
||||||
|
new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => {
|
||||||
|
const testDoc = await this.localDatabase.getDBEntry(file.path, { conflicts: true });
|
||||||
|
if (testDoc === false) {
|
||||||
|
Logger("Missing file..", LOG_LEVEL.VERBOSE);
|
||||||
|
return res(true);
|
||||||
|
}
|
||||||
|
if (!testDoc._conflicts) {
|
||||||
|
Logger("Nothing have to do with this conflict", LOG_LEVEL.VERBOSE);
|
||||||
|
return res(true);
|
||||||
|
}
|
||||||
|
const toDelete = selected;
|
||||||
|
const toKeep = conflictCheckResult.left.rev != toDelete ? conflictCheckResult.left.rev : conflictCheckResult.right.rev;
|
||||||
|
if (toDelete == "") {
|
||||||
|
//concat both,
|
||||||
|
// write data,and delete both old rev.
|
||||||
|
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
||||||
|
await this.app.vault.modify(file, p);
|
||||||
|
await this.updateIntoDB(file);
|
||||||
|
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev });
|
||||||
|
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev });
|
||||||
|
await this.pullFile(file.path);
|
||||||
|
Logger("concat both file");
|
||||||
|
setTimeout(() => {
|
||||||
|
//resolved, check again.
|
||||||
|
this.showIfConflicted(file);
|
||||||
|
}, 500);
|
||||||
|
} else if (toDelete == null) {
|
||||||
|
Logger("Leave it still conflicted");
|
||||||
|
} else {
|
||||||
|
Logger(`resolved conflict:${file.path}`);
|
||||||
|
await this.localDatabase.deleteDBEntry(file.path, { rev: toDelete });
|
||||||
|
await this.pullFile(file.path, null, true, toKeep);
|
||||||
|
setTimeout(() => {
|
||||||
|
//resolved, check again.
|
||||||
|
this.showIfConflicted(file);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res(true);
|
||||||
|
}).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
conflictedCheckFiles: string[] = [];
|
||||||
|
|
||||||
|
// queueing the conflicted file check
|
||||||
|
conflictedCheckTimer: number;
|
||||||
|
queueConflictedCheck(file: TFile) {
|
||||||
|
this.conflictedCheckFiles = this.conflictedCheckFiles.filter((e) => e != file.path);
|
||||||
|
this.conflictedCheckFiles.push(file.path);
|
||||||
|
if (this.conflictedCheckTimer != null) {
|
||||||
|
window.clearTimeout(this.conflictedCheckTimer);
|
||||||
|
}
|
||||||
|
this.conflictedCheckTimer = window.setTimeout(async () => {
|
||||||
|
this.conflictedCheckTimer = null;
|
||||||
|
const checkFiles = JSON.parse(JSON.stringify(this.conflictedCheckFiles)) as string[];
|
||||||
|
for (const filename of checkFiles) {
|
||||||
|
try {
|
||||||
|
const file = this.app.vault.getAbstractFileByPath(filename);
|
||||||
|
if (file != null && file instanceof TFile) {
|
||||||
|
await this.showIfConflicted(file);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
async showIfConflicted(file: TFile) {
|
async showIfConflicted(file: TFile) {
|
||||||
await runWithLock("conflicted", false, async () => {
|
await runWithLock("conflicted", false, async () => {
|
||||||
const conflictCheckResult = await this.getConflictedStatus(file.path);
|
const conflictCheckResult = await this.getConflictedStatus(file.path);
|
||||||
if (conflictCheckResult === false) return; //nothign to do.
|
if (conflictCheckResult === false) {
|
||||||
|
//nothign to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (conflictCheckResult === true) {
|
if (conflictCheckResult === true) {
|
||||||
//auto resolved, but need check again;
|
//auto resolved, but need check again;
|
||||||
|
Logger("conflict:Automatically merged, but we have to check it again");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.showIfConflicted(file);
|
this.showIfConflicted(file);
|
||||||
}, 500);
|
}, 500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//there conflicts, and have to resolve ;
|
//there conflicts, and have to resolve ;
|
||||||
const leaf = this.app.workspace.activeLeaf;
|
await this.showMergeDialog(file, conflictCheckResult);
|
||||||
if (leaf) {
|
|
||||||
new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => {
|
|
||||||
const testDoc = await this.localDatabase.getDBEntry(file.path, { conflicts: true });
|
|
||||||
if (testDoc === false) return;
|
|
||||||
if (!testDoc._conflicts) {
|
|
||||||
Logger("something went wrong on merging.", LOG_LEVEL.NOTICE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const toDelete = selected;
|
|
||||||
if (toDelete == null) {
|
|
||||||
//concat both,
|
|
||||||
// write data,and delete both old rev.
|
|
||||||
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
|
||||||
await this.app.vault.modify(file, p);
|
|
||||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev });
|
|
||||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (toDelete == "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Logger(`resolved conflict:${file.path}`);
|
|
||||||
await this.localDatabase.deleteDBEntry(file.path, { rev: toDelete });
|
|
||||||
await this.pullFile(file.path, null, true);
|
|
||||||
setTimeout(() => {
|
|
||||||
//resolved, check again.
|
|
||||||
this.showIfConflicted(file);
|
|
||||||
}, 500);
|
|
||||||
}).open();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
||||||
if (!fileList) {
|
const targetFile = this.app.vault.getAbstractFileByPath(id2path(filename));
|
||||||
fileList = this.app.vault.getFiles();
|
if (targetFile == null) {
|
||||||
}
|
|
||||||
const targetFiles = fileList.filter((e) => e.path == id2path(filename));
|
|
||||||
if (targetFiles.length == 0) {
|
|
||||||
//have to create;
|
//have to create;
|
||||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||||
if (doc === false) return;
|
if (doc === false) return;
|
||||||
await this.doc2storage_create(doc, force);
|
await this.doc2storage_create(doc, force);
|
||||||
} else if (targetFiles.length == 1) {
|
} else if (targetFile instanceof TFile) {
|
||||||
//normal case
|
//normal case
|
||||||
const file = targetFiles[0];
|
const file = targetFile;
|
||||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||||
if (doc === false) return;
|
if (doc === false) return;
|
||||||
await this.doc2storate_modify(doc, file, force);
|
await this.doc2storate_modify(doc, file, force);
|
||||||
} else {
|
} else {
|
||||||
Logger(`target files:${filename} is two or more files in your vault`);
|
Logger(`target files:${filename} is exists as the folder`);
|
||||||
//something went wrong..
|
//something went wrong..
|
||||||
}
|
}
|
||||||
//when to opened file;
|
//when to opened file;
|
||||||
@@ -1105,17 +1162,21 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
children: [],
|
children: [],
|
||||||
datatype: datatype,
|
datatype: datatype,
|
||||||
};
|
};
|
||||||
//From here
|
//upsert should locked
|
||||||
|
const isNotChanged = await runWithLock("file:" + fullpath, false, async () => {
|
||||||
const old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
|
const old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
|
||||||
if (old !== false) {
|
if (old !== false) {
|
||||||
const oldData = { data: old.data, deleted: old._deleted };
|
const oldData = { data: old.data, deleted: old._deleted };
|
||||||
const newData = { data: d.data, deleted: d._deleted };
|
const newData = { data: d.data, deleted: d._deleted };
|
||||||
if (JSON.stringify(oldData) == JSON.stringify(newData)) {
|
if (JSON.stringify(oldData) == JSON.stringify(newData)) {
|
||||||
Logger("not changed:" + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
Logger("not changed:" + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
// d._rev = old._rev;
|
// d._rev = old._rev;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (isNotChanged) return;
|
||||||
await this.localDatabase.putDBEntry(d);
|
await this.localDatabase.putDBEntry(d);
|
||||||
|
|
||||||
Logger("put database:" + fullpath + "(" + datatype + ") ");
|
Logger("put database:" + fullpath + "(" + datatype + ") ");
|
||||||
@@ -1167,7 +1228,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
return { plugins, allPlugins, thisDevicePlugins };
|
return { plugins, allPlugins, thisDevicePlugins };
|
||||||
}
|
}
|
||||||
async sweepPlugin(showMessage = false) {
|
async sweepPlugin(showMessage = false) {
|
||||||
console.log(`pluginSync:${this.settings.usePluginSync}`);
|
|
||||||
if (!this.settings.usePluginSync) return;
|
if (!this.settings.usePluginSync) return;
|
||||||
await runWithLock("sweepplugin", false, async () => {
|
await runWithLock("sweepplugin", false, async () => {
|
||||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||||
|
|||||||
15
src/utils.ts
15
src/utils.ts
@@ -114,16 +114,16 @@ function objectToKey(key: any): string {
|
|||||||
const keys = Object.keys(key).sort((a, b) => a.localeCompare(b));
|
const keys = Object.keys(key).sort((a, b) => a.localeCompare(b));
|
||||||
return keys.map((e) => e + objectToKey(key[e])).join(":");
|
return keys.map((e) => e + objectToKey(key[e])).join(":");
|
||||||
}
|
}
|
||||||
// Just run some async/await as like transacion SERIALIZABLE
|
|
||||||
|
|
||||||
|
// Just run async/await as like transacion ISOLATION SERIALIZABLE
|
||||||
export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: () => Promise<T>): Promise<T> {
|
export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: () => Promise<T>): Promise<T> {
|
||||||
Logger(`Lock:${key}:enter`, LOG_LEVEL.VERBOSE);
|
// Logger(`Lock:${key}:enter`, LOG_LEVEL.VERBOSE);
|
||||||
const lockKey = typeof key === "string" ? key : objectToKey(key);
|
const lockKey = typeof key === "string" ? key : objectToKey(key);
|
||||||
const handleNextProcs = () => {
|
const handleNextProcs = () => {
|
||||||
if (typeof pendingProcs[lockKey] === "undefined") {
|
if (typeof pendingProcs[lockKey] === "undefined") {
|
||||||
//simply unlock
|
//simply unlock
|
||||||
runningProcs.remove(lockKey);
|
runningProcs.remove(lockKey);
|
||||||
Logger(`Lock:${lockKey}:released`, LOG_LEVEL.VERBOSE);
|
// Logger(`Lock:${lockKey}:released`, LOG_LEVEL.VERBOSE);
|
||||||
} else {
|
} else {
|
||||||
Logger(`Lock:${lockKey}:left ${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
Logger(`Lock:${lockKey}:left ${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
||||||
let nextProc = null;
|
let nextProc = null;
|
||||||
@@ -143,6 +143,10 @@ export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: (
|
|||||||
handleNextProcs();
|
handleNextProcs();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) {
|
||||||
|
delete pendingProcs[lockKey];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -164,7 +168,7 @@ export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: (
|
|||||||
new Promise<void>((res, rej) => {
|
new Promise<void>((res, rej) => {
|
||||||
proc()
|
proc()
|
||||||
.then((v) => {
|
.then((v) => {
|
||||||
Logger(`Lock:${key}:processed`, LOG_LEVEL.VERBOSE);
|
// Logger(`Lock:${key}:processed`, LOG_LEVEL.VERBOSE);
|
||||||
handleNextProcs();
|
handleNextProcs();
|
||||||
responderRes(v);
|
responderRes(v);
|
||||||
res();
|
res();
|
||||||
@@ -178,10 +182,11 @@ export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: (
|
|||||||
});
|
});
|
||||||
|
|
||||||
pendingProcs[lockKey].push(subproc);
|
pendingProcs[lockKey].push(subproc);
|
||||||
|
// Logger(`Lock:${lockKey}:queud:left${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
||||||
return responder;
|
return responder;
|
||||||
} else {
|
} else {
|
||||||
runningProcs.push(lockKey);
|
runningProcs.push(lockKey);
|
||||||
Logger(`Lock:${lockKey}:aqquired`, LOG_LEVEL.VERBOSE);
|
// Logger(`Lock:${lockKey}:aqquired`, LOG_LEVEL.VERBOSE);
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
proc()
|
proc()
|
||||||
.then((v) => {
|
.then((v) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user