mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-05 23:32:00 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8969b1800a | ||
|
|
2c8e026e29 | ||
|
|
a6c27eab3d | ||
|
|
9b5c57d540 | ||
|
|
c251c596e8 | ||
|
|
61188cfaef | ||
|
|
97d944fd75 | ||
|
|
5802ed31be |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.22.0",
|
"version": "0.22.2",
|
||||||
"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.22.0",
|
"version": "0.22.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.22.0",
|
"version": "0.22.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.22.0",
|
"version": "0.22.2",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { normalizePath, type PluginManifest } from "./deps";
|
import { normalizePath, type PluginManifest } from "./deps";
|
||||||
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry } from "./lib/src/types";
|
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry } from "./lib/src/types";
|
||||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
||||||
import { createBinaryBlob, delay, isDocContentSame } from "./lib/src/utils";
|
import { createBinaryBlob, isDocContentSame, sendSignal } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||||
import { isInternalMetadata, PeriodicProcessor } from "./utils";
|
import { isInternalMetadata, PeriodicProcessor } from "./utils";
|
||||||
@@ -86,7 +86,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
await this.syncInternalFilesAndDatabase("pull", false, false, filenames);
|
await this.syncInternalFilesAndDatabase("pull", false, false, filenames);
|
||||||
Logger(`DONE :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE);
|
Logger(`DONE :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE);
|
||||||
return;
|
return;
|
||||||
}, { batchSize: 100, concurrentLimit: 1, delay: 100, yieldThreshold: 10, suspended: false, totalRemainingReactiveSource: hiddenFilesEventCount }
|
}, { batchSize: 100, concurrentLimit: 1, delay: 10, yieldThreshold: 10, suspended: false, totalRemainingReactiveSource: hiddenFilesEventCount }
|
||||||
);
|
);
|
||||||
|
|
||||||
recentProcessedInternalFiles = [] as string[];
|
recentProcessedInternalFiles = [] as string[];
|
||||||
@@ -134,25 +134,34 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
async resolveConflictOnInternalFiles() {
|
async resolveConflictOnInternalFiles() {
|
||||||
// Scan all conflicted internal files
|
// Scan all conflicted internal files
|
||||||
const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true });
|
const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true });
|
||||||
for await (const doc of conflicted) {
|
this.conflictResolutionProcessor.suspend();
|
||||||
if (!("_conflicts" in doc))
|
try {
|
||||||
continue;
|
for await (const doc of conflicted) {
|
||||||
if (isInternalMetadata(doc._id)) {
|
if (!("_conflicts" in doc))
|
||||||
await this.resolveConflictOnInternalFile(doc.path);
|
continue;
|
||||||
|
if (isInternalMetadata(doc._id)) {
|
||||||
|
this.conflictResolutionProcessor.enqueue(doc.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("something went wrong on resolving all conflicted internal files");
|
||||||
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
|
await this.conflictResolutionProcessor.startPipeline().waitForPipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveConflictOnInternalFile(path: FilePathWithPrefix): Promise<boolean> {
|
conflictResolutionProcessor = new QueueProcessor(async (paths: FilePathWithPrefix[]) => {
|
||||||
|
const path = paths[0];
|
||||||
|
sendSignal(`cancel-internal-conflict:${path}`);
|
||||||
try {
|
try {
|
||||||
// Retrieve data
|
// Retrieve data
|
||||||
const id = await this.path2id(path, ICHeader);
|
const id = await this.path2id(path, ICHeader);
|
||||||
const doc = await this.localDatabase.getRaw(id, { conflicts: true });
|
const doc = await this.localDatabase.getRaw(id, { conflicts: true });
|
||||||
// If there is no conflict, return with false.
|
// If there is no conflict, return with false.
|
||||||
if (!("_conflicts" in doc))
|
if (!("_conflicts" in doc))
|
||||||
return false;
|
return;
|
||||||
if (doc._conflicts.length == 0)
|
if (doc._conflicts.length == 0)
|
||||||
return false;
|
return;
|
||||||
Logger(`Hidden file conflicted:${path}`);
|
Logger(`Hidden file conflicted:${path}`);
|
||||||
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
||||||
const revA = doc._rev;
|
const revA = doc._rev;
|
||||||
@@ -177,21 +186,12 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
await this.storeInternalFileToDatabase({ path: filename, ...stat });
|
await this.storeInternalFileToDatabase({ path: filename, ...stat });
|
||||||
await this.extractInternalFileFromDatabase(filename);
|
await this.extractInternalFileFromDatabase(filename);
|
||||||
await this.localDatabase.removeRaw(id, revB);
|
await this.localDatabase.removeRaw(id, revB);
|
||||||
return this.resolveConflictOnInternalFile(path);
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
|
return [{ path, revA, revB }];
|
||||||
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
|
|
||||||
const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB });
|
|
||||||
if (docAMerge != false && docBMerge != false) {
|
|
||||||
if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) {
|
|
||||||
await delay(200);
|
|
||||||
// Again for other conflicted revisions.
|
|
||||||
return this.resolveConflictOnInternalFile(path);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const revBDoc = await this.localDatabase.getRaw(id, { rev: revB });
|
const revBDoc = await this.localDatabase.getRaw(id, { rev: revB });
|
||||||
// determine which revision should been deleted.
|
// determine which revision should been deleted.
|
||||||
@@ -205,12 +205,31 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
await this.localDatabase.removeRaw(id, delRev);
|
await this.localDatabase.removeRaw(id, delRev);
|
||||||
Logger(`Older one has been deleted:${path}`);
|
Logger(`Older one has been deleted:${path}`);
|
||||||
// check the file again
|
// check the file again
|
||||||
return this.resolveConflictOnInternalFile(path);
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
|
return;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Failed to resolve conflict (Hidden): ${path}`);
|
Logger(`Failed to resolve conflict (Hidden): ${path}`);
|
||||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, yieldThreshold: 10,
|
||||||
|
pipeTo: new QueueProcessor(async (results) => {
|
||||||
|
const { path, revA, revB } = results[0]
|
||||||
|
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
|
||||||
|
const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB });
|
||||||
|
if (docAMerge != false && docBMerge != false) {
|
||||||
|
if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) {
|
||||||
|
// Again for other conflicted revisions.
|
||||||
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false, yieldThreshold: 10 })
|
||||||
|
})
|
||||||
|
|
||||||
|
queueConflictCheck(path: FilePathWithPrefix) {
|
||||||
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
||||||
@@ -595,7 +614,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
|
|
||||||
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
|
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
|
||||||
return serialized("conflict:merge-data", () => new Promise((res) => {
|
return new Promise((res) => {
|
||||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||||
const docs = [docA, docB];
|
const docs = [docA, docB];
|
||||||
const path = stripAllPrefixes(docA.path);
|
const path = stripAllPrefixes(docA.path);
|
||||||
@@ -649,7 +668,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
modal.open();
|
modal.open();
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanInternalFiles(): Promise<InternalFileInfo[]> {
|
async scanInternalFiles(): Promise<InternalFileInfo[]> {
|
||||||
|
|||||||
@@ -1,23 +1,41 @@
|
|||||||
import { App, Modal } from "./deps";
|
import { App, Modal } from "./deps";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||||
import { type diff_result } from "./lib/src/types";
|
import { CANCELLED, LEAVE_TO_SUBSEQUENT, RESULT_TIMED_OUT, type diff_result } from "./lib/src/types";
|
||||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||||
|
import { delay, sendValue, waitForValue } from "./lib/src/utils";
|
||||||
|
|
||||||
|
export type MergeDialogResult = typeof LEAVE_TO_SUBSEQUENT | typeof CANCELLED | string;
|
||||||
export class ConflictResolveModal extends Modal {
|
export class ConflictResolveModal extends Modal {
|
||||||
// result: Array<[number, string]>;
|
|
||||||
result: diff_result;
|
result: diff_result;
|
||||||
filename: string;
|
filename: string;
|
||||||
callback: (remove_rev: string) => Promise<void>;
|
|
||||||
|
|
||||||
constructor(app: App, filename: string, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
|
response: MergeDialogResult = CANCELLED;
|
||||||
|
isClosed = false;
|
||||||
|
consumed = false;
|
||||||
|
|
||||||
|
constructor(app: App, filename: string, diff: diff_result) {
|
||||||
super(app);
|
super(app);
|
||||||
this.result = diff;
|
this.result = diff;
|
||||||
this.callback = callback;
|
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
|
// Send cancel signal for the previous merge dialogue
|
||||||
|
// if not there, simply be ignored.
|
||||||
|
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||||
|
sendValue("cancel-resolve-conflict:" + this.filename, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
// Send cancel signal for the previous merge dialogue
|
||||||
|
// if not there, simply be ignored.
|
||||||
|
sendValue("cancel-resolve-conflict:" + this.filename, true);
|
||||||
|
setTimeout(async () => {
|
||||||
|
const forceClose = await waitForValue("cancel-resolve-conflict:" + this.filename);
|
||||||
|
// debugger;
|
||||||
|
if (forceClose) {
|
||||||
|
this.sendResponse(CANCELLED);
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||||
this.titleEl.setText("Conflicting changes");
|
this.titleEl.setText("Conflicting changes");
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
contentEl.createEl("span", { text: this.filename });
|
contentEl.createEl("span", { text: this.filename });
|
||||||
@@ -44,42 +62,32 @@ export class ConflictResolveModal extends Modal {
|
|||||||
div2.innerHTML = `
|
div2.innerHTML = `
|
||||||
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
|
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
|
||||||
`;
|
`;
|
||||||
contentEl.createEl("button", { text: "Keep A" }, (e) => {
|
contentEl.createEl("button", { text: "Keep A" }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.right.rev)));
|
||||||
e.addEventListener("click", async () => {
|
contentEl.createEl("button", { text: "Keep B" }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.left.rev)));
|
||||||
const callback = this.callback;
|
contentEl.createEl("button", { text: "Concat both" }, (e) => e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT)));
|
||||||
this.callback = null;
|
contentEl.createEl("button", { text: "Not now" }, (e) => e.addEventListener("click", () => this.sendResponse(CANCELLED)));
|
||||||
this.close();
|
}
|
||||||
await callback(this.result.right.rev);
|
|
||||||
});
|
sendResponse(result: MergeDialogResult) {
|
||||||
});
|
this.response = result;
|
||||||
contentEl.createEl("button", { text: "Keep B" }, (e) => {
|
this.close();
|
||||||
e.addEventListener("click", async () => {
|
|
||||||
const callback = this.callback;
|
|
||||||
this.callback = null;
|
|
||||||
this.close();
|
|
||||||
await callback(this.result.left.rev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
contentEl.createEl("button", { text: "Concat both" }, (e) => {
|
|
||||||
e.addEventListener("click", async () => {
|
|
||||||
const callback = this.callback;
|
|
||||||
this.callback = null;
|
|
||||||
this.close();
|
|
||||||
await callback("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
contentEl.createEl("button", { text: "Not now" }, (e) => {
|
|
||||||
e.addEventListener("click", () => {
|
|
||||||
this.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
if (this.callback != null) {
|
if (this.consumed) {
|
||||||
this.callback(null);
|
return;
|
||||||
}
|
}
|
||||||
|
this.consumed = true;
|
||||||
|
sendValue("close-resolve-conflict:" + this.filename, this.response);
|
||||||
|
sendValue("cancel-resolve-conflict:" + this.filename, false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async waitForResult(): Promise<MergeDialogResult> {
|
||||||
|
await delay(100);
|
||||||
|
const r = await waitForValue<MergeDialogResult>("close-resolve-conflict:" + this.filename);
|
||||||
|
if (r === RESULT_TIMED_OUT) return CANCELLED;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,34 @@ import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL_I
|
|||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
||||||
import { getDocData } from "./lib/src/utils";
|
import { getDocData } from "./lib/src/utils";
|
||||||
import { stripPrefix } from "./lib/src/path";
|
import { isPlainText, stripPrefix } from "./lib/src/path";
|
||||||
|
|
||||||
|
function isImage(path: string) {
|
||||||
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
||||||
|
return ["png", "jpg", "jpeg", "gif", "bmp", "webp"].includes(ext);
|
||||||
|
}
|
||||||
|
function isComparableText(path: string) {
|
||||||
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
||||||
|
return isPlainText(path) || ["md", "mdx", "txt", "json"].includes(ext);
|
||||||
|
}
|
||||||
|
function isComparableTextDecode(path: string) {
|
||||||
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
||||||
|
return ["json"].includes(ext)
|
||||||
|
}
|
||||||
|
function readDocument(w: LoadedEntry) {
|
||||||
|
if (isImage(w.path)) {
|
||||||
|
return new Uint8Array(decodeBinary(w.data));
|
||||||
|
}
|
||||||
|
if (w.data == "plain") return getDocData(w.data);
|
||||||
|
if (isComparableTextDecode(w.path)) return readString(new Uint8Array(decodeBinary(w.data)));
|
||||||
|
if (isComparableText(w.path)) return getDocData(w.data);
|
||||||
|
try {
|
||||||
|
return readString(new Uint8Array(decodeBinary(w.data)));
|
||||||
|
} catch (ex) {
|
||||||
|
// NO OP.
|
||||||
|
}
|
||||||
|
return getDocData(w.data);
|
||||||
|
}
|
||||||
export class DocumentHistoryModal extends Modal {
|
export class DocumentHistoryModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
range!: HTMLInputElement;
|
range!: HTMLInputElement;
|
||||||
@@ -56,7 +82,6 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
this.range.max = "0";
|
this.range.max = "0";
|
||||||
this.range.value = "";
|
this.range.value = "";
|
||||||
this.range.disabled = true;
|
this.range.disabled = true;
|
||||||
this.showDiff
|
|
||||||
this.contentView.setText(`History of this file was not recorded.`);
|
this.contentView.setText(`History of this file was not recorded.`);
|
||||||
} else {
|
} else {
|
||||||
this.contentView.setText(`Error occurred.`);
|
this.contentView.setText(`Error occurred.`);
|
||||||
@@ -76,6 +101,22 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
const rev = this.revs_info[index];
|
const rev = this.revs_info[index];
|
||||||
await this.showExactRev(rev.rev);
|
await this.showExactRev(rev.rev);
|
||||||
}
|
}
|
||||||
|
BlobURLs = new Map<string, string>();
|
||||||
|
|
||||||
|
revokeURL(key: string) {
|
||||||
|
const v = this.BlobURLs.get(key);
|
||||||
|
if (v) {
|
||||||
|
URL.revokeObjectURL(v);
|
||||||
|
}
|
||||||
|
this.BlobURLs.set(key, undefined);
|
||||||
|
}
|
||||||
|
generateBlobURL(key: string, data: Uint8Array) {
|
||||||
|
this.revokeURL(key);
|
||||||
|
const v = URL.createObjectURL(new Blob([data], { endings: "transparent", type: "application/octet-stream" }));
|
||||||
|
this.BlobURLs.set(key, v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
async showExactRev(rev: string) {
|
async showExactRev(rev: string) {
|
||||||
const db = this.plugin.localDatabase;
|
const db = this.plugin.localDatabase;
|
||||||
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||||
@@ -88,42 +129,67 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
} else {
|
} else {
|
||||||
this.currentDoc = w;
|
this.currentDoc = w;
|
||||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||||
let result = "";
|
let result = undefined;
|
||||||
const w1data = w.datatype == "plain" ? getDocData(w.data) : readString(new Uint8Array(decodeBinary(w.data)));
|
const w1data = readDocument(w);
|
||||||
this.currentDeleted = !!w.deleted;
|
this.currentDeleted = !!w.deleted;
|
||||||
this.currentText = w1data;
|
// this.currentText = w1data;
|
||||||
if (this.showDiff) {
|
if (this.showDiff) {
|
||||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||||
const oldRev = this.revs_info[prevRevIdx].rev;
|
const oldRev = this.revs_info[prevRevIdx].rev;
|
||||||
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
||||||
if (w2 != false) {
|
if (w2 != false) {
|
||||||
const dmp = new diff_match_patch();
|
if (typeof w1data == "string") {
|
||||||
const w2data = w2.datatype == "plain" ? getDocData(w2.data) : readString(new Uint8Array(decodeBinary(w2.data)));
|
result = "";
|
||||||
const diff = dmp.diff_main(w2data, w1data);
|
const dmp = new diff_match_patch();
|
||||||
dmp.diff_cleanupSemantic(diff);
|
const w2data = readDocument(w2) as string;
|
||||||
for (const v of diff) {
|
const diff = dmp.diff_main(w2data, w1data);
|
||||||
const x1 = v[0];
|
dmp.diff_cleanupSemantic(diff);
|
||||||
const x2 = v[1];
|
for (const v of diff) {
|
||||||
if (x1 == DIFF_DELETE) {
|
const x1 = v[0];
|
||||||
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
|
const x2 = v[1];
|
||||||
} else if (x1 == DIFF_EQUAL) {
|
if (x1 == DIFF_DELETE) {
|
||||||
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
|
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
|
||||||
} else if (x1 == DIFF_INSERT) {
|
} else if (x1 == DIFF_EQUAL) {
|
||||||
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
|
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
|
||||||
|
} else if (x1 == DIFF_INSERT) {
|
||||||
|
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
result = result.replace(/\n/g, "<br>");
|
||||||
|
} else if (isImage(this.file)) {
|
||||||
|
const src = this.generateBlobURL("base", w1data);
|
||||||
|
const overlay = this.generateBlobURL("overlay", readDocument(w2) as Uint8Array);
|
||||||
|
result =
|
||||||
|
`<div class='ls-imgdiff-wrap'>
|
||||||
|
<div class='overlay'>
|
||||||
|
<img class='img-base' src="${src}">
|
||||||
|
<img class='img-overlay' src='${overlay}'>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
this.contentView.removeClass("op-pre");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result = result.replace(/\n/g, "<br>");
|
}
|
||||||
} else {
|
if (result == undefined) {
|
||||||
result = escapeStringToHTML(w1data);
|
if (typeof w1data != "string") {
|
||||||
|
if (isImage(this.file)) {
|
||||||
|
const src = this.generateBlobURL("base", w1data);
|
||||||
|
result =
|
||||||
|
`<div class='ls-imgdiff-wrap'>
|
||||||
|
<div class='overlay'>
|
||||||
|
<img class='img-base' src="${src}">
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
this.contentView.removeClass("op-pre");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = escapeStringToHTML(w1data);
|
result = escapeStringToHTML(w1data);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result = escapeStringToHTML(w1data);
|
|
||||||
}
|
}
|
||||||
|
if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file";
|
||||||
this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
|
this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,5 +283,9 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
onClose() {
|
onClose() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
|
this.BlobURLs.forEach(value => {
|
||||||
|
console.log(value);
|
||||||
|
if (value) URL.revokeObjectURL(value);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { App, Modal } from "./deps";
|
import { App, Modal } from "./deps";
|
||||||
import { type FilePath, type LoadedEntry } from "./lib/src/types";
|
import { type FilePath, type LoadedEntry } from "./lib/src/types";
|
||||||
import JsonResolvePane from "./JsonResolvePane.svelte";
|
import JsonResolvePane from "./JsonResolvePane.svelte";
|
||||||
|
import { waitForSignal } from "./lib/src/utils";
|
||||||
|
|
||||||
export class JsonResolveModal extends Modal {
|
export class JsonResolveModal extends Modal {
|
||||||
// result: Array<[number, string]>;
|
// result: Array<[number, string]>;
|
||||||
@@ -20,6 +21,7 @@ export class JsonResolveModal extends Modal {
|
|||||||
this.nameA = nameA;
|
this.nameA = nameA;
|
||||||
this.nameB = nameB;
|
this.nameB = nameB;
|
||||||
this.defaultSelect = defaultSelect;
|
this.defaultSelect = defaultSelect;
|
||||||
|
waitForSignal(`cancel-internal-conflict:${filename}`).then(() => this.close());
|
||||||
}
|
}
|
||||||
async UICallback(keepRev: string, mergedStr?: string) {
|
async UICallback(keepRev: string, mergedStr?: string) {
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb";
|
|||||||
import { testCrypt } from "./lib/src/e2ee_v2";
|
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
|
import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
|
||||||
|
import type { ButtonComponent } from "obsidian";
|
||||||
|
|
||||||
|
|
||||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||||
@@ -34,6 +35,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
|
|
||||||
|
// const preferred_setting = isCloudantURI(this.plugin.settings.couchDB_URI) ? PREFERRED_SETTING_CLOUDANT : PREFERRED_SETTING_SELF_HOSTED;
|
||||||
|
// const default_setting = { ...DEFAULT_SETTINGS };
|
||||||
|
|
||||||
|
|
||||||
containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." });
|
containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." });
|
||||||
containerEl.addClass("sls-setting");
|
containerEl.addClass("sls-setting");
|
||||||
containerEl.removeClass("isWizard");
|
containerEl.removeClass("isWizard");
|
||||||
@@ -819,6 +824,53 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
text.inputEl.setAttribute("type", "number");
|
text.inputEl.setAttribute("type", "number");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
containerGeneralSettingsEl.createEl("h4", { text: "Share settings via markdown" });
|
||||||
|
let settingSyncFile = this.plugin.settings.settingSyncFile;
|
||||||
|
let buttonApplyFilename: ButtonComponent;
|
||||||
|
new Setting(containerGeneralSettingsEl)
|
||||||
|
.setName("Filename")
|
||||||
|
.setDesc("If you set this, all settings are saved in a markdown file. You will also be notified when new settings were arrived. You can set different files by the platform.")
|
||||||
|
.addText((text) => {
|
||||||
|
text.setPlaceholder("livesync/setting.md")
|
||||||
|
.setValue(settingSyncFile)
|
||||||
|
.onChange((value) => {
|
||||||
|
settingSyncFile = value;
|
||||||
|
if (settingSyncFile == this.plugin.settings.settingSyncFile) {
|
||||||
|
buttonApplyFilename.removeCta()
|
||||||
|
buttonApplyFilename.setDisabled(true);
|
||||||
|
} else {
|
||||||
|
buttonApplyFilename.setCta()
|
||||||
|
buttonApplyFilename.setDisabled(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).addButton(button => {
|
||||||
|
button.setButtonText("Apply")
|
||||||
|
.onClick(async () => {
|
||||||
|
this.plugin.settings.settingSyncFile = settingSyncFile;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
})
|
||||||
|
buttonApplyFilename = button;
|
||||||
|
})
|
||||||
|
new Setting(containerGeneralSettingsEl)
|
||||||
|
.setName("Write credentials in the file")
|
||||||
|
.setDesc("(Not recommended) If set, credentials will be stored in the file.")
|
||||||
|
.addToggle(toggle => {
|
||||||
|
toggle.setValue(this.plugin.settings.writeCredentialsForSettingSync)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.writeCredentialsForSettingSync = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
new Setting(containerGeneralSettingsEl)
|
||||||
|
.setName("Notify all setting files")
|
||||||
|
.addToggle(toggle => {
|
||||||
|
toggle.setValue(this.plugin.settings.notifyAllSettingSyncFile)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.notifyAllSettingSyncFile = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
containerGeneralSettingsEl.createEl("h4", { text: "Advanced Confidentiality" });
|
containerGeneralSettingsEl.createEl("h4", { text: "Advanced Confidentiality" });
|
||||||
|
|
||||||
@@ -1116,7 +1168,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Postpone resolution of unopened files")
|
.setName("Postpone resolution of inactive files")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.checkConflictOnlyOnOpen).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.checkConflictOnlyOnOpen).onChange(async (value) => {
|
||||||
@@ -1124,6 +1176,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Postpone manual resolution of inactive files")
|
||||||
|
.setClass("wizardHidden")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.showMergeDialogOnlyOnActive).onChange(async (value) => {
|
||||||
|
this.plugin.settings.showMergeDialogOnlyOnActive = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
containerSyncSettingEl.createEl("h4", { text: "Compatibility" }).addClass("wizardHidden");
|
containerSyncSettingEl.createEl("h4", { text: "Compatibility" }).addClass("wizardHidden");
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Always resolve conflict manually")
|
.setName("Always resolve conflict manually")
|
||||||
@@ -1229,7 +1290,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
let skipPatternTextArea: TextAreaComponent = null;
|
let skipPatternTextArea: TextAreaComponent = null;
|
||||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
||||||
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$";
|
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$";
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Folders and files to ignore")
|
.setName("Folders and files to ignore")
|
||||||
.setDesc(
|
.setDesc(
|
||||||
@@ -1313,11 +1374,35 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
|
|
||||||
containerSyncSettingEl.createEl("h4", {
|
containerSyncSettingEl.createEl("h4", {
|
||||||
text: sanitizeHTMLToDom(`Synchronization target filters`),
|
text: sanitizeHTMLToDom(`Targets`),
|
||||||
}).addClass("wizardHidden");
|
}).addClass("wizardHidden");
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Regular expression to ignore files")
|
.setName("Synchronising files")
|
||||||
.setDesc("If this is set, any changes to local and remote files that match this will be skipped.")
|
.setDesc("(RegExp) Empty to sync all files. set filter as a regular expression to limit synchronising files.")
|
||||||
|
.setClass("wizardHidden")
|
||||||
|
.addTextArea((text) => {
|
||||||
|
text
|
||||||
|
.setValue(this.plugin.settings.syncOnlyRegEx)
|
||||||
|
.setPlaceholder("\\.md$|\\.txt")
|
||||||
|
.onChange(async (value) => {
|
||||||
|
let isValidRegExp = false;
|
||||||
|
try {
|
||||||
|
new RegExp(value);
|
||||||
|
isValidRegExp = true;
|
||||||
|
} catch (_) {
|
||||||
|
// NO OP.
|
||||||
|
}
|
||||||
|
if (isValidRegExp || value.trim() == "") {
|
||||||
|
this.plugin.settings.syncOnlyRegEx = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Non-Synchronising files")
|
||||||
|
.setDesc("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addTextArea((text) => {
|
.addTextArea((text) => {
|
||||||
text
|
text
|
||||||
@@ -1340,29 +1425,22 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Regular expression for restricting synchronization targets")
|
.setName("Maximum file size")
|
||||||
.setDesc("If this is set, changes to local and remote files that only match this will be processed.")
|
.setDesc("(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addTextArea((text) => {
|
.addText((text) => {
|
||||||
text
|
text.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.syncOnlyRegEx)
|
.setValue(this.plugin.settings.syncMaxSizeInMB + "")
|
||||||
.setPlaceholder("\\.md$|\\.txt")
|
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
let isValidRegExp = false;
|
let v = Number(value);
|
||||||
try {
|
if (isNaN(v) || v < 1) {
|
||||||
new RegExp(value);
|
v = 0;
|
||||||
isValidRegExp = true;
|
|
||||||
} catch (_) {
|
|
||||||
// NO OP.
|
|
||||||
}
|
}
|
||||||
if (isValidRegExp || value.trim() == "") {
|
this.plugin.settings.syncMaxSizeInMB = v;
|
||||||
this.plugin.settings.syncOnlyRegEx = value;
|
await this.plugin.saveSettings();
|
||||||
await this.plugin.saveSettings();
|
});
|
||||||
}
|
text.inputEl.setAttribute("type", "number");
|
||||||
})
|
});
|
||||||
return text;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("(Beta) Use ignore files")
|
.setName("(Beta) Use ignore files")
|
||||||
.setDesc("If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.")
|
.setDesc("If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.")
|
||||||
@@ -1403,15 +1481,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
}).addClass("wizardHidden");
|
}).addClass("wizardHidden");
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Batch size")
|
.setName("Batch size")
|
||||||
.setDesc("Number of change feed items to process at a time. Defaults to 50.")
|
.setDesc("Number of change feed items to process at a time. Defaults to 50. Minimum is 2.")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder("")
|
text.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.batch_size + "")
|
.setValue(this.plugin.settings.batch_size + "")
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
let v = Number(value);
|
let v = Number(value);
|
||||||
if (isNaN(v) || v < 10) {
|
if (isNaN(v) || v < 2) {
|
||||||
v = 10;
|
v = 2;
|
||||||
}
|
}
|
||||||
this.plugin.settings.batch_size = v;
|
this.plugin.settings.batch_size = v;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
@@ -1421,15 +1499,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Batch limit")
|
.setName("Batch limit")
|
||||||
.setDesc("Number of batches to process at a time. Defaults to 40. This along with batch size controls how many docs are kept in memory at a time.")
|
.setDesc("Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder("")
|
text.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.batches_limit + "")
|
.setValue(this.plugin.settings.batches_limit + "")
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
let v = Number(value);
|
let v = Number(value);
|
||||||
if (isNaN(v) || v < 10) {
|
if (isNaN(v) || v < 2) {
|
||||||
v = 10;
|
v = 2;
|
||||||
}
|
}
|
||||||
this.plugin.settings.batches_limit = v;
|
this.plugin.settings.batches_limit = v;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
@@ -1526,8 +1604,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
pluginConfig.encryptedPassphrase = REDACTED;
|
pluginConfig.encryptedPassphrase = REDACTED;
|
||||||
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
||||||
pluginConfig.pluginSyncExtendedSetting = {};
|
pluginConfig.pluginSyncExtendedSetting = {};
|
||||||
|
const obsidianInfo = navigator.userAgent;
|
||||||
const msgConfig = `----remote config----
|
const msgConfig = `---- Obsidian info ----
|
||||||
|
${obsidianInfo}
|
||||||
|
---- remote config ----
|
||||||
${stringifyYaml(responseConfig)}
|
${stringifyYaml(responseConfig)}
|
||||||
---- Plug-in config ---
|
---- Plug-in config ---
|
||||||
version:${manifestVersion}
|
version:${manifestVersion}
|
||||||
@@ -1644,7 +1724,7 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
|
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
|
||||||
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
|
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
|
||||||
}
|
}
|
||||||
await this.plugin.showIfConflicted(docName as FilePathWithPrefix);
|
await this.plugin.queueConflictCheck(docName as FilePathWithPrefix);
|
||||||
} else {
|
} else {
|
||||||
Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE);
|
Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE);
|
||||||
Logger(ret, LOG_LEVEL_VERBOSE);
|
Logger(ret, LOG_LEVEL_VERBOSE);
|
||||||
|
|||||||
@@ -27,8 +27,10 @@
|
|||||||
async function requestReload() {
|
async function requestReload() {
|
||||||
await addOn.reloadPluginList(true);
|
await addOn.reloadPluginList(true);
|
||||||
}
|
}
|
||||||
|
let allTerms = [] as string[];
|
||||||
pluginList.subscribe((e) => {
|
pluginList.subscribe((e) => {
|
||||||
list = e;
|
list = e;
|
||||||
|
allTerms = unique(list.map((e) => e.term));
|
||||||
});
|
});
|
||||||
pluginIsEnumerating.subscribe((e) => {
|
pluginIsEnumerating.subscribe((e) => {
|
||||||
loading = e;
|
loading = e;
|
||||||
@@ -172,7 +174,7 @@
|
|||||||
.filter((e) => `${e.category}/${e.name}` == key)
|
.filter((e) => `${e.category}/${e.name}` == key)
|
||||||
.map((e) => e.files)
|
.map((e) => e.files)
|
||||||
.flat()
|
.flat()
|
||||||
.map((e) => e.filename)
|
.map((e) => e.filename),
|
||||||
);
|
);
|
||||||
automaticList.set(key, mode);
|
automaticList.set(key, mode);
|
||||||
automaticListDisp = automaticList;
|
automaticListDisp = automaticList;
|
||||||
@@ -218,6 +220,16 @@
|
|||||||
.sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name))
|
.sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name))
|
||||||
.reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record<string, string[]>);
|
.reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record<string, string[]>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deleteTerm = "";
|
||||||
|
|
||||||
|
async function deleteAllItems(term: string) {
|
||||||
|
const deleteItems = list.filter((e) => e.term == term);
|
||||||
|
for (const item of deleteItems) {
|
||||||
|
await deleteData(item);
|
||||||
|
}
|
||||||
|
addOn.reloadPluginList(true);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -322,6 +334,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if isMaintenanceMode}
|
||||||
|
<div class="list">
|
||||||
|
<div>
|
||||||
|
<h3>Maintenance Commands</h3>
|
||||||
|
<div class="maintenancerow">
|
||||||
|
<label for="">Delete All of </label>
|
||||||
|
<select bind:value={deleteTerm}>
|
||||||
|
{#each allTerms as term}
|
||||||
|
<option value={term}>{term}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="status"
|
||||||
|
on:click={(evt) => {
|
||||||
|
deleteAllItems(deleteTerm);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label>
|
<label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,4 +458,13 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 3em;
|
min-height: 3em;
|
||||||
}
|
}
|
||||||
|
.maintenancerow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.maintenancerow label {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "
|
|||||||
import { serialized } from "./lib/src/lock";
|
import { serialized } from "./lib/src/lock";
|
||||||
import type { FilePath } from "./lib/src/types";
|
import type { FilePath } from "./lib/src/types";
|
||||||
import { createBinaryBlob, isDocContentSame } from "./lib/src/utils";
|
import { createBinaryBlob, isDocContentSame } from "./lib/src/utils";
|
||||||
|
import type { InternalFileInfo } from "./types";
|
||||||
function getFileLockKey(file: TFile | TFolder | string) {
|
function getFileLockKey(file: TFile | TFolder | string) {
|
||||||
return `fl:${typeof (file) == "string" ? file : file.path}`;
|
return `fl:${typeof (file) == "string" ? file : file.path}`;
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,7 @@ export class SerializedFileAccess {
|
|||||||
} else {
|
} else {
|
||||||
return await serialized(getFileLockKey(file), async () => {
|
return await serialized(getFileLockKey(file), async () => {
|
||||||
const oldData = await this.app.vault.readBinary(file);
|
const oldData = await this.app.vault.readBinary(file);
|
||||||
if (isDocContentSame(createBinaryBlob(oldData), createBinaryBlob(data))) {
|
if (await isDocContentSame(createBinaryBlob(oldData), createBinaryBlob(data))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await this.app.vault.modifyBinary(file, toArrayBuffer(data), options)
|
await this.app.vault.modifyBinary(file, toArrayBuffer(data), options)
|
||||||
@@ -121,8 +122,8 @@ export class SerializedFileAccess {
|
|||||||
this.touchedFiles.unshift(key);
|
this.touchedFiles.unshift(key);
|
||||||
this.touchedFiles = this.touchedFiles.slice(0, 100);
|
this.touchedFiles = this.touchedFiles.slice(0, 100);
|
||||||
}
|
}
|
||||||
recentlyTouched(file: TFile) {
|
recentlyTouched(file: TFile | InternalFileInfo) {
|
||||||
const key = `${file.path}-${file.stat.mtime}-${file.stat.size}`;
|
const key = file instanceof TFile ? `${file.path}-${file.stat.mtime}-${file.stat.size}` : `${file.path}-${file.mtime}-${file.size}`;
|
||||||
if (this.touchedFiles.indexOf(key) == -1) return false;
|
if (this.touchedFiles.indexOf(key) == -1) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { SerializedFileAccess } from "./SerializedFileAccess";
|
import type { SerializedFileAccess } from "./SerializedFileAccess";
|
||||||
import { Plugin, TAbstractFile, TFile, TFolder } from "./deps";
|
import { Plugin, TAbstractFile, TFile, TFolder } from "./deps";
|
||||||
|
import { Logger } from "./lib/src/logger";
|
||||||
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
||||||
import type { KeyedQueueProcessor } from "./lib/src/processor";
|
import type { KeyedQueueProcessor } from "./lib/src/processor";
|
||||||
import { type FilePath, type ObsidianLiveSyncSettings } from "./lib/src/types";
|
import { LOG_LEVEL_NOTICE, type FilePath, type ObsidianLiveSyncSettings } from "./lib/src/types";
|
||||||
import { type FileEventItem, type FileEventType, type FileInfo, type InternalFileInfo } from "./types";
|
import { type FileEventItem, type FileEventType, type FileInfo, type InternalFileInfo } from "./types";
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +18,8 @@ type LiveSyncForStorageEventManager = Plugin &
|
|||||||
vaultAccess: SerializedFileAccess
|
vaultAccess: SerializedFileAccess
|
||||||
} & {
|
} & {
|
||||||
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
|
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
|
||||||
fileEventQueue: KeyedQueueProcessor<FileEventItem, any>
|
fileEventQueue: KeyedQueueProcessor<FileEventItem, any>,
|
||||||
|
isFileSizeExceeded: (size: number) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +98,11 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
const type = param.type;
|
const type = param.type;
|
||||||
const file = param.file;
|
const file = param.file;
|
||||||
const oldPath = param.oldPath;
|
const oldPath = param.oldPath;
|
||||||
|
const size = file instanceof TFile ? file.stat.size : (file as InternalFileInfo)?.size ?? 0;
|
||||||
|
if (this.plugin.isFileSizeExceeded(size) && (type == "CREATE" || type == "CHANGED")) {
|
||||||
|
Logger(`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`, LOG_LEVEL_NOTICE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (file instanceof TFolder) continue;
|
if (file instanceof TFolder) continue;
|
||||||
if (!await this.plugin.isTargetFile(file.path)) continue;
|
if (!await this.plugin.isTargetFile(file.path)) continue;
|
||||||
if (this.plugin.settings.suspendFileWatching) continue;
|
if (this.plugin.settings.suspendFileWatching) continue;
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 33f7e69433...ee376a80a5
655
src/main.ts
655
src/main.ts
@@ -1,10 +1,10 @@
|
|||||||
const isDebug = false;
|
const isDebug = false;
|
||||||
|
|
||||||
import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "./deps";
|
import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch, stringifyYaml, parseYaml } from "./deps";
|
||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl, type MarkdownFileInfo } from "./deps";
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl, type MarkdownFileInfo } from "./deps";
|
||||||
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE, type SavingEntry, } from "./lib/src/types";
|
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE, type SavingEntry, MISSING_OR_ERROR, NOT_CONFLICTED, AUTO_MERGED, CANCELLED, LEAVE_TO_SUBSEQUENT, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, } from "./lib/src/types";
|
||||||
import { type InternalFileInfo, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
|
import { type InternalFileInfo, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
|
||||||
import { createBinaryBlob, createTextBlob, fireAndForget, getDocData, isDocContentSame, sendValue } from "./lib/src/utils";
|
import { createBinaryBlob, createTextBlob, fireAndForget, getDocData, isDocContentSame, isObjectDifferent, sendValue } from "./lib/src/utils";
|
||||||
import { Logger, setGlobalLogFunction } from "./lib/src/logger";
|
import { Logger, setGlobalLogFunction } from "./lib/src/logger";
|
||||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||||
@@ -13,11 +13,11 @@ import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
|||||||
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, isValidPath, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, getPath, getPathWithoutPrefix, getPathFromTFile, performRebuildDB, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject, isCustomisationSyncMetadata } from "./utils";
|
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, isValidPath, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, getPath, getPathWithoutPrefix, getPathFromTFile, performRebuildDB, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject, isCustomisationSyncMetadata } from "./utils";
|
||||||
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
||||||
import { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb";
|
import { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb";
|
||||||
import { lockStats, logStore, type LogEntry, collectingChunks, pluginScanningCount, hiddenFilesProcessingCount, hiddenFilesEventCount, logMessages } from "./lib/src/stores";
|
import { logStore, type LogEntry, collectingChunks, pluginScanningCount, hiddenFilesProcessingCount, hiddenFilesEventCount, logMessages } from "./lib/src/stores";
|
||||||
import { setNoticeClass } from "./lib/src/wrapper";
|
import { setNoticeClass } from "./lib/src/wrapper";
|
||||||
import { versionNumberString2Number, writeString, decodeBinary, readString } from "./lib/src/strbin";
|
import { versionNumberString2Number, writeString, decodeBinary, readString } from "./lib/src/strbin";
|
||||||
import { addPrefix, isAcceptedAll, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
|
import { addPrefix, isAcceptedAll, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
|
||||||
import { isLockAcquired, serialized, skipIfDuplicated } from "./lib/src/lock";
|
import { isLockAcquired, serialized, shareRunningResult, skipIfDuplicated } from "./lib/src/lock";
|
||||||
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
|
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
|
||||||
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
|
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
|
||||||
import { LiveSyncDBReplicator, type LiveSyncReplicatorEnv } from "./lib/src/LiveSyncReplicator";
|
import { LiveSyncDBReplicator, type LiveSyncReplicatorEnv } from "./lib/src/LiveSyncReplicator";
|
||||||
@@ -63,6 +63,10 @@ async function fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse>
|
|||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SETTING_HEADER = "````yaml:livesync-setting\n";
|
||||||
|
const SETTING_FOOTER = "\n````";
|
||||||
|
|
||||||
export default class ObsidianLiveSyncPlugin extends Plugin
|
export default class ObsidianLiveSyncPlugin extends Plugin
|
||||||
implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv {
|
implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv {
|
||||||
|
|
||||||
@@ -299,38 +303,36 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
return timer;
|
return timer;
|
||||||
}
|
}
|
||||||
|
|
||||||
isRedFlagRaised(): boolean {
|
isFlagFileExist(path: string) {
|
||||||
const redflag = this.vaultAccess.getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG));
|
const redflag = this.vaultAccess.getAbstractFileByPath(normalizePath(path));
|
||||||
if (redflag != null) {
|
if (redflag != null && redflag instanceof TFile) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
isRedFlag2Raised(): boolean {
|
async deleteFlagFile(path: string) {
|
||||||
const redflag = this.vaultAccess.getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG2));
|
try {
|
||||||
if (redflag != null) {
|
const redflag = this.vaultAccess.getAbstractFileByPath(normalizePath(path));
|
||||||
return true;
|
if (redflag != null && redflag instanceof TFile) {
|
||||||
|
await this.vaultAccess.delete(redflag, true);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`Could not delete ${path}`);
|
||||||
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
isRedFlagRaised = () => this.isFlagFileExist(FLAGMD_REDFLAG)
|
||||||
|
isRedFlag2Raised = () => this.isFlagFileExist(FLAGMD_REDFLAG2) || this.isFlagFileExist(FLAGMD_REDFLAG2_HR)
|
||||||
|
isRedFlag3Raised = () => this.isFlagFileExist(FLAGMD_REDFLAG3) || this.isFlagFileExist(FLAGMD_REDFLAG3_HR)
|
||||||
|
|
||||||
async deleteRedFlag2() {
|
async deleteRedFlag2() {
|
||||||
const redflag = this.vaultAccess.getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG2));
|
await this.deleteFlagFile(FLAGMD_REDFLAG2);
|
||||||
if (redflag != null && redflag instanceof TFile) {
|
await this.deleteFlagFile(FLAGMD_REDFLAG2_HR);
|
||||||
await this.vaultAccess.delete(redflag, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isRedFlag3Raised(): boolean {
|
|
||||||
const redflag = this.vaultAccess.getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG3));
|
|
||||||
if (redflag != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteRedFlag3() {
|
async deleteRedFlag3() {
|
||||||
const redflag = this.vaultAccess.getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG3));
|
await this.deleteFlagFile(FLAGMD_REDFLAG3);
|
||||||
if (redflag != null && redflag instanceof TFile) {
|
await this.deleteFlagFile(FLAGMD_REDFLAG3_HR);
|
||||||
await this.vaultAccess.delete(redflag, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID) {
|
showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID) {
|
||||||
@@ -365,7 +367,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
const target = await askSelectString(this.app, "File to resolve conflict", notesList);
|
const target = await askSelectString(this.app, "File to resolve conflict", notesList);
|
||||||
if (target) {
|
if (target) {
|
||||||
const targetItem = notes.find(e => e.dispPath == target);
|
const targetItem = notes.find(e => e.dispPath == target);
|
||||||
await this.resolveConflicted(targetItem.path);
|
this.resolveConflicted(targetItem.path);
|
||||||
|
await this.conflictCheckQueue.waitForPipeline();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -373,13 +376,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
|
|
||||||
async resolveConflicted(target: FilePathWithPrefix) {
|
async resolveConflicted(target: FilePathWithPrefix) {
|
||||||
if (isInternalMetadata(target)) {
|
if (isInternalMetadata(target)) {
|
||||||
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
|
this.addOnHiddenFileSync.queueConflictCheck(target);
|
||||||
} else if (isPluginMetadata(target)) {
|
} else if (isPluginMetadata(target)) {
|
||||||
await this.resolveConflictByNewerEntry(target);
|
await this.resolveConflictByNewerEntry(target);
|
||||||
} else if (isCustomisationSyncMetadata(target)) {
|
} else if (isCustomisationSyncMetadata(target)) {
|
||||||
await this.resolveConflictByNewerEntry(target);
|
await this.resolveConflictByNewerEntry(target);
|
||||||
} else {
|
} else {
|
||||||
await this.showIfConflicted(target);
|
this.queueConflictCheck(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,7 +428,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
this.settings.suspendFileWatching = true;
|
this.settings.suspendFileWatching = true;
|
||||||
await this.saveSettings();
|
await this.saveSettings();
|
||||||
if (this.isRedFlag2Raised()) {
|
if (this.isRedFlag2Raised()) {
|
||||||
Logger(`${FLAGMD_REDFLAG2} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`, LOG_LEVEL_NOTICE);
|
Logger(`${FLAGMD_REDFLAG2} or ${FLAGMD_REDFLAG2_HR} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`, LOG_LEVEL_NOTICE);
|
||||||
await this.addOnSetup.rebuildEverything();
|
await this.addOnSetup.rebuildEverything();
|
||||||
await this.deleteRedFlag2();
|
await this.deleteRedFlag2();
|
||||||
if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") {
|
if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") {
|
||||||
@@ -435,7 +438,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
this.app.commands.executeCommandById("app:reload")
|
this.app.commands.executeCommandById("app:reload")
|
||||||
}
|
}
|
||||||
} else if (this.isRedFlag3Raised()) {
|
} else if (this.isRedFlag3Raised()) {
|
||||||
Logger(`${FLAGMD_REDFLAG3} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, LOG_LEVEL_NOTICE);
|
Logger(`${FLAGMD_REDFLAG3} or ${FLAGMD_REDFLAG3_HR} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, LOG_LEVEL_NOTICE);
|
||||||
await this.addOnSetup.fetchLocal();
|
await this.addOnSetup.fetchLocal();
|
||||||
await this.deleteRedFlag3();
|
await this.deleteRedFlag3();
|
||||||
if (this.settings.suspendFileWatching) {
|
if (this.settings.suspendFileWatching) {
|
||||||
@@ -583,10 +586,10 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-checkdoc-conflicted",
|
id: "livesync-checkdoc-conflicted",
|
||||||
name: "Resolve if conflicted.",
|
name: "Resolve if conflicted.",
|
||||||
editorCallback: async (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
||||||
const file = view.file;
|
const file = view.file;
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
await this.showIfConflicted(getPathFromTFile(file));
|
this.queueConflictCheck(file);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -623,9 +626,10 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-history",
|
id: "livesync-history",
|
||||||
name: "Show history",
|
name: "Show history",
|
||||||
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
|
callback: () => {
|
||||||
if (view.file) this.showHistory(view.file, null);
|
const file = this.app.workspace.getActiveFile();
|
||||||
},
|
if (file) this.showHistory(file, null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-scan-files",
|
id: "livesync-scan-files",
|
||||||
@@ -677,6 +681,28 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
this.showGlobalHistory()
|
this.showGlobalHistory()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.addCommand({
|
||||||
|
id: "livesync-export-config",
|
||||||
|
name: "Write setting markdown manually",
|
||||||
|
checkCallback: (checking) => {
|
||||||
|
if (checking) {
|
||||||
|
return this.settings.settingSyncFile != "";
|
||||||
|
}
|
||||||
|
this.saveSettingData();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.addCommand({
|
||||||
|
id: "livesync-import-config",
|
||||||
|
name: "Parse setting file",
|
||||||
|
editorCheckCallback: (checking, editor, ctx) => {
|
||||||
|
if (checking) {
|
||||||
|
const doc = editor.getValue();
|
||||||
|
const ret = this.extractSettingFromWholeText(doc);
|
||||||
|
return ret.body != "";
|
||||||
|
}
|
||||||
|
this.checkAndApplySettingFromMarkdown(ctx.file.path, false);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
this.registerView(
|
this.registerView(
|
||||||
VIEW_TYPE_GLOBAL_HISTORY,
|
VIEW_TYPE_GLOBAL_HISTORY,
|
||||||
@@ -769,6 +795,9 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
|
cancelAllPeriodicTask();
|
||||||
|
cancelAllTasks();
|
||||||
|
this._unloaded = true;
|
||||||
for (const addOn of this.addOns) {
|
for (const addOn of this.addOns) {
|
||||||
addOn.onunload();
|
addOn.onunload();
|
||||||
}
|
}
|
||||||
@@ -780,9 +809,6 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
this.replicator.closeReplication();
|
this.replicator.closeReplication();
|
||||||
this.localDatabase.close();
|
this.localDatabase.close();
|
||||||
}
|
}
|
||||||
cancelAllPeriodicTask();
|
|
||||||
cancelAllTasks();
|
|
||||||
this._unloaded = true;
|
|
||||||
Logger("unloading plugin");
|
Logger("unloading plugin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -942,9 +968,163 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
this.localDatabase.settings = this.settings;
|
this.localDatabase.settings = this.settings;
|
||||||
this.fileEventQueue.delay = this.settings.batchSave ? 5000 : 100;
|
this.fileEventQueue.delay = this.settings.batchSave ? 5000 : 100;
|
||||||
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
||||||
|
if (this.settings.settingSyncFile != "") {
|
||||||
|
fireAndForget(() => this.saveSettingToMarkdown(this.settings.settingSyncFile));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extractSettingFromWholeText(data: string): { preamble: string, body: string, postscript: string } {
|
||||||
|
if (data.indexOf(SETTING_HEADER) === -1) {
|
||||||
|
return {
|
||||||
|
preamble: data,
|
||||||
|
body: "",
|
||||||
|
postscript: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const startMarkerPos = data.indexOf(SETTING_HEADER);
|
||||||
|
const dataStartPos = startMarkerPos == -1 ? data.length : startMarkerPos;
|
||||||
|
const endMarkerPos = startMarkerPos == -1 ? data.length : data.indexOf(SETTING_FOOTER, dataStartPos);
|
||||||
|
const dataEndPos = endMarkerPos == -1 ? data.length : endMarkerPos;
|
||||||
|
const body = data.substring(dataStartPos + SETTING_HEADER.length, dataEndPos);
|
||||||
|
const ret = {
|
||||||
|
preamble: data.substring(0, dataStartPos),
|
||||||
|
body,
|
||||||
|
postscript: data.substring(dataEndPos + SETTING_FOOTER.length + 1)
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSettingFromMarkdown(filename: string, data?: string) {
|
||||||
|
const file = this.app.vault.getAbstractFileByPath(filename);
|
||||||
|
if (!(file instanceof TFile)) return {
|
||||||
|
preamble: "",
|
||||||
|
body: "",
|
||||||
|
postscript: "",
|
||||||
|
};
|
||||||
|
if (data) {
|
||||||
|
return this.extractSettingFromWholeText(data);
|
||||||
|
}
|
||||||
|
const parseData = data ?? await this.app.vault.read(file);
|
||||||
|
return this.extractSettingFromWholeText(parseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAndApplySettingFromMarkdown(filename: string, automated?: boolean) {
|
||||||
|
if (automated && !this.settings.notifyAllSettingSyncFile) {
|
||||||
|
if (this.settings.settingSyncFile != filename) {
|
||||||
|
Logger(`Setting file (${filename}) is not matched to the current configuration. skipped.`, LOG_LEVEL_INFO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { body } = await this.parseSettingFromMarkdown(filename);
|
||||||
|
let newSetting = {} as Partial<ObsidianLiveSyncSettings>;
|
||||||
|
try {
|
||||||
|
newSetting = parseYaml(body);
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("Could not parse YAML", LOG_LEVEL_NOTICE);
|
||||||
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("settingSyncFile" in newSetting && newSetting.settingSyncFile != filename) {
|
||||||
|
Logger("This setting file seems to backed up one. Please fix the filename or settingSyncFile value.", automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let settingToApply = { ...DEFAULT_SETTINGS } as ObsidianLiveSyncSettings;
|
||||||
|
settingToApply = { ...settingToApply, ...newSetting }
|
||||||
|
if (!(settingToApply?.writeCredentialsForSettingSync)) {
|
||||||
|
//New setting does not contains credentials.
|
||||||
|
settingToApply.couchDB_USER = this.settings.couchDB_USER;
|
||||||
|
settingToApply.couchDB_PASSWORD = this.settings.couchDB_PASSWORD;
|
||||||
|
settingToApply.passphrase = this.settings.passphrase;
|
||||||
|
}
|
||||||
|
const oldSetting = this.generateSettingForMarkdown(this.settings, settingToApply.writeCredentialsForSettingSync);
|
||||||
|
if (!isObjectDifferent(oldSetting, this.generateSettingForMarkdown(settingToApply))) {
|
||||||
|
Logger("Setting markdown has been detected, but not changed.", automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const addMsg = this.settings.settingSyncFile != filename ? " (This is not-active file)" : "";
|
||||||
|
this.askInPopup("apply-setting-from-md", `Setting markdown ${filename}${addMsg} has been detected. Apply this from {HERE}.`, (anchor) => {
|
||||||
|
anchor.text = "HERE";
|
||||||
|
anchor.addEventListener("click", async () => {
|
||||||
|
const APPLY_ONLY = "Apply settings";
|
||||||
|
const APPLY_AND_RESTART = "Apply settings and restart obsidian";
|
||||||
|
const APPLY_AND_REBUILD = "Apply settings and restart obsidian with red_flag_rebuild.md";
|
||||||
|
const APPLY_AND_FETCH = "Apply settings and restart obsidian with red_flag_fetch.md";
|
||||||
|
const CANCEL = "Cancel";
|
||||||
|
const result = await askSelectString(this.app, "Ready for apply the setting.", [APPLY_AND_RESTART, APPLY_ONLY, APPLY_AND_FETCH, APPLY_AND_REBUILD, CANCEL]);
|
||||||
|
if (result == APPLY_ONLY || result == APPLY_AND_RESTART || result == APPLY_AND_REBUILD || result == APPLY_AND_FETCH) {
|
||||||
|
this.settings = settingToApply;
|
||||||
|
await this.saveSettingData();
|
||||||
|
if (result == APPLY_ONLY) {
|
||||||
|
Logger("Loaded settings have been applied!", LOG_LEVEL_NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result == APPLY_AND_REBUILD) {
|
||||||
|
await this.app.vault.create(FLAGMD_REDFLAG2_HR, "");
|
||||||
|
}
|
||||||
|
if (result == APPLY_AND_FETCH) {
|
||||||
|
await this.app.vault.create(FLAGMD_REDFLAG3_HR, "");
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
this.app.commands.executeCommandById("app:reload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
generateSettingForMarkdown(settings?: ObsidianLiveSyncSettings, keepCredential?: boolean): Partial<ObsidianLiveSyncSettings> {
|
||||||
|
const saveData = { ...(settings ? settings : this.settings) };
|
||||||
|
delete saveData.encryptedCouchDBConnection;
|
||||||
|
delete saveData.encryptedPassphrase;
|
||||||
|
if (!saveData.writeCredentialsForSettingSync && !keepCredential) {
|
||||||
|
delete saveData.couchDB_USER;
|
||||||
|
delete saveData.couchDB_PASSWORD;
|
||||||
|
delete saveData.passphrase;
|
||||||
|
}
|
||||||
|
return saveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettingToMarkdown(filename: string) {
|
||||||
|
const saveData = this.generateSettingForMarkdown();
|
||||||
|
let file = this.app.vault.getAbstractFileByPath(filename);
|
||||||
|
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
await this.ensureDirectoryEx(filename);
|
||||||
|
const initialContent = `This file contains Self-hosted LiveSync settings as YAML.
|
||||||
|
Except for the \`livesync-setting\` code block, we can add a note for free.
|
||||||
|
|
||||||
|
If the name of this file matches the value of the "settingSyncFile" setting inside the \`livesync-setting\` block, LiveSync will tell us whenever the settings change. We can decide to accept or decline the remote setting. (In other words, we can back up this file by renaming it to another name).
|
||||||
|
|
||||||
|
We can perform a command in this file.
|
||||||
|
- \`Parse setting file\` : load the setting from the file.
|
||||||
|
|
||||||
|
**Note** Please handle it with all of your care if you have configured to write credentials in.
|
||||||
|
|
||||||
|
|
||||||
|
`
|
||||||
|
file = await this.app.vault.create(filename, initialContent + SETTING_HEADER + "\n" + SETTING_FOOTER);
|
||||||
|
}
|
||||||
|
if (!(file instanceof TFile)) {
|
||||||
|
Logger(`Markdown Setting: ${filename} already exists as a folder`, LOG_LEVEL_NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this.app.vault.read(file);
|
||||||
|
const { preamble, body, postscript } = this.extractSettingFromWholeText(data);
|
||||||
|
const newBody = stringifyYaml(saveData);
|
||||||
|
|
||||||
|
if (newBody == body) {
|
||||||
|
Logger("Markdown setting: Nothing had been changed", LOG_LEVEL_VERBOSE);
|
||||||
|
} else {
|
||||||
|
await this.app.vault.modify(file, preamble + SETTING_HEADER + newBody + SETTING_FOOTER + postscript);
|
||||||
|
Logger(`Markdown setting: ${filename} has been updated!`, LOG_LEVEL_VERBOSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
await this.saveSettingData();
|
await this.saveSettingData();
|
||||||
fireAndForget(() => this.realizeSettingSyncMode());
|
fireAndForget(() => this.realizeSettingSyncMode());
|
||||||
@@ -1092,6 +1272,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
|
|
||||||
const cache = queue.args.cache;
|
const cache = queue.args.cache;
|
||||||
if (queue.type == "CREATE" || queue.type == "CHANGED") {
|
if (queue.type == "CREATE" || queue.type == "CHANGED") {
|
||||||
|
fireAndForget(() => this.checkAndApplySettingFromMarkdown(queue.args.file.path, true));
|
||||||
const keyD1 = `file-last-proc-DELETED-${file.path}`;
|
const keyD1 = `file-last-proc-DELETED-${file.path}`;
|
||||||
await this.kvDB.set(keyD1, mtime);
|
await this.kvDB.set(keyD1, mtime);
|
||||||
if (!await this.updateIntoDB(targetFile, false, cache)) {
|
if (!await this.updateIntoDB(targetFile, false, cache)) {
|
||||||
@@ -1138,7 +1319,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
if (this.settings.syncOnFileOpen && !this.suspended) {
|
if (this.settings.syncOnFileOpen && !this.suspended) {
|
||||||
await this.replicate();
|
await this.replicate();
|
||||||
}
|
}
|
||||||
await this.showIfConflicted(getPathFromTFile(file));
|
this.queueConflictCheck(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyBatchChange() {
|
async applyBatchChange() {
|
||||||
@@ -1272,7 +1453,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async doc2storage(docEntry: EntryBody, file?: TFile, force?: boolean) {
|
async processEntryDoc(docEntry: EntryBody, file: TFile | undefined, force?: boolean) {
|
||||||
const mode = file == undefined ? "create" : "modify";
|
const mode = file == undefined ? "create" : "modify";
|
||||||
|
|
||||||
const path = this.getPath(docEntry);
|
const path = this.getPath(docEntry);
|
||||||
@@ -1280,37 +1461,49 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!await this.isTargetFile(path)) return;
|
if (!await this.isTargetFile(path)) return;
|
||||||
if (docEntry._deleted || docEntry.deleted) {
|
|
||||||
// This occurs not only when files are deleted, but also when conflicts are resolved.
|
// Conflict resolution check
|
||||||
// We have to check no other revisions are left.
|
const existDoc = await this.localDatabase.getDBEntry(path, { conflicts: true });
|
||||||
const existDoc = await this.localDatabase.getDBEntry(path, { conflicts: true });
|
const msg = `STORAGE <- DB (${mode}${force ? ",force" : ""},${existDoc ? existDoc?.datatype : "--"}) `;
|
||||||
|
// let performPullFileAgain = false;
|
||||||
|
if (existDoc && existDoc._conflicts) {
|
||||||
|
if (this.settings.writeDocumentsIfConflicted) {
|
||||||
|
Logger(`Processing: ${file.path}: Conflicted revision has been deleted, but there were more conflicts. `, LOG_LEVEL_INFO);
|
||||||
|
await this.processEntryDoc(docEntry, file, true);
|
||||||
|
return;
|
||||||
|
} else if (force != true) {
|
||||||
|
Logger(`Processing: ${file.path}: Conflicted revision has been deleted, but there were more conflicts...`);
|
||||||
|
this.queueConflictCheck(file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there are no conflicts, or forced to overwrite.
|
||||||
|
|
||||||
|
if (docEntry._deleted || docEntry.deleted || existDoc === false) {
|
||||||
if (path != file.path) {
|
if (path != file.path) {
|
||||||
Logger(`delete skipped: ${file.path} :Not exactly matched`, LOG_LEVEL_VERBOSE);
|
Logger(`delete skipped: ${file.path} :Not exactly matched`, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
if (existDoc === false) {
|
if (existDoc === false) {
|
||||||
await this.deleteVaultItem(file);
|
await this.deleteVaultItem(file);
|
||||||
} else {
|
} else {
|
||||||
if (existDoc._conflicts) {
|
// Conflict has been resolved at this time,
|
||||||
if (this.settings.writeDocumentsIfConflicted) {
|
await this.pullFile(path, null, force);
|
||||||
Logger(`Delete: ${file.path}: Conflicted revision has been deleted, but there were more conflicts. `, LOG_LEVEL_INFO);
|
|
||||||
await this.pullFile(path, null, true);
|
|
||||||
} else {
|
|
||||||
Logger(`Delete: ${file.path}: Conflicted revision has been deleted, but there were more conflicts...`);
|
|
||||||
this.queueConflictedOnlyActiveFile(file);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger(`Delete: ${file.path}: Conflict revision has been deleted and resolved`);
|
|
||||||
await this.pullFile(path, null, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const localMtime = ~~((file?.stat?.mtime || 0) / 1000);
|
const localMtime = ~~((file?.stat?.mtime || 0) / 1000);
|
||||||
const docMtime = ~~(docEntry.mtime / 1000);
|
const docMtime = ~~(docEntry.mtime / 1000);
|
||||||
|
|
||||||
const doc = await this.localDatabase.getDBEntry(path, { rev: docEntry._rev });
|
// const doc = await this.localDatabase.getDBEntry(path, { rev: docEntry._rev });
|
||||||
if (doc === false) return;
|
// if (doc === false) return;
|
||||||
const msg = `STORAGE <- DB (${mode}${force ? ",force" : ""},${doc.datatype}) `;
|
const doc = existDoc;
|
||||||
|
// if (doc === false) {
|
||||||
|
// // The latest file
|
||||||
|
// await this.pullFile(path, null, force);
|
||||||
|
// // Logger(`delete skipped: ${file.path} :Not exactly matched`, LOG_LEVEL_VERBOSE);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
if (doc.datatype != "newnote" && doc.datatype != "plain") {
|
if (doc.datatype != "newnote" && doc.datatype != "plain") {
|
||||||
Logger(msg + "ERROR, Invalid datatype: " + path + "(" + doc.datatype + ")", LOG_LEVEL_NOTICE);
|
Logger(msg + "ERROR, Invalid datatype: " + path + "(" + doc.datatype + ")", LOG_LEVEL_NOTICE);
|
||||||
return;
|
return;
|
||||||
@@ -1367,21 +1560,16 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queueConflictCheck(file: FilePathWithPrefix | TFile) {
|
||||||
queuedEntries: EntryBody[] = [];
|
const path = file instanceof TFile ? getPathFromTFile(file) : file;
|
||||||
|
if (this.settings.checkConflictOnlyOnOpen) {
|
||||||
queueConflictedOnlyActiveFile(file: TFile) {
|
|
||||||
if (!this.settings.checkConflictOnlyOnOpen) {
|
|
||||||
this.queueConflictedCheck(file);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
const af = this.app.workspace.getActiveFile();
|
const af = this.app.workspace.getActiveFile();
|
||||||
if (af && af.path == file.path) {
|
if (af && af.path != path) {
|
||||||
this.queueConflictedCheck(file);
|
Logger(`${file} is conflicted, merging process has been postponed.`, LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
this.conflictCheckQueue.enqueue(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveQueuedFiles() {
|
saveQueuedFiles() {
|
||||||
@@ -1428,34 +1616,15 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
storageApplyingProcessor = new KeyedQueueProcessor(async (docs: LoadedEntry[]) => {
|
storageApplyingProcessor = new KeyedQueueProcessor(async (docs: LoadedEntry[]) => {
|
||||||
const entry = docs[0];
|
const entry = docs[0];
|
||||||
const path = this.getPath(entry);
|
const path = this.getPath(entry);
|
||||||
Logger(`Applying ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) change...`, LOG_LEVEL_VERBOSE);
|
Logger(`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) change...`, LOG_LEVEL_VERBOSE);
|
||||||
const targetFile = this.vaultAccess.getAbstractFileByPath(this.getPathWithoutPrefix(entry));
|
const targetFile = this.vaultAccess.getAbstractFileByPath(this.getPathWithoutPrefix(entry));
|
||||||
if (targetFile == null) {
|
if (targetFile instanceof TFolder) {
|
||||||
if (entry._deleted || entry.deleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const doc = entry;
|
|
||||||
await this.doc2storage(doc);
|
|
||||||
} else if (targetFile instanceof TFile) {
|
|
||||||
const doc = entry;
|
|
||||||
const file = targetFile;
|
|
||||||
if (this.settings.writeDocumentsIfConflicted) {
|
|
||||||
await this.doc2storage(doc, file);
|
|
||||||
this.queueConflictedOnlyActiveFile(file);
|
|
||||||
} else {
|
|
||||||
const d = await this.localDatabase.getDBEntryMeta(this.getPath(entry), { conflicts: true }, true);
|
|
||||||
if (d && !d._conflicts) {
|
|
||||||
await this.doc2storage(doc, file);
|
|
||||||
} else {
|
|
||||||
if (!this.queueConflictedOnlyActiveFile(file)) {
|
|
||||||
Logger(`${this.getPath(entry)} is conflicted, write to the storage has been postponed.`, LOG_LEVEL_NOTICE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger(`${this.getPath(entry)} is already exist as the folder`);
|
Logger(`${this.getPath(entry)} is already exist as the folder`);
|
||||||
|
} else {
|
||||||
|
await this.processEntryDoc(entry, targetFile instanceof TFile ? targetFile : undefined);
|
||||||
|
Logger(`Processing ${path} (${entry._id.substring(0, 8)}:${entry._rev?.substring(0, 5)}) `);
|
||||||
}
|
}
|
||||||
Logger(`Applied ${path} (${entry._id.substring(0, 8)}:${entry._rev?.substring(0, 5)}) change...`);
|
|
||||||
return;
|
return;
|
||||||
}, { suspended: true, batchSize: 1, concurrentLimit: 2, yieldThreshold: 1, delay: 0, totalRemainingReactiveSource: this.storageApplyingCount }).startPipeline()
|
}, { suspended: true, batchSize: 1, concurrentLimit: 2, yieldThreshold: 1, delay: 0, totalRemainingReactiveSource: this.storageApplyingCount }).startPipeline()
|
||||||
|
|
||||||
@@ -1492,6 +1661,11 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
if (this.databaseQueuedProcessor._isSuspended) {
|
if (this.databaseQueuedProcessor._isSuspended) {
|
||||||
Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL_INFO);
|
Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL_INFO);
|
||||||
}
|
}
|
||||||
|
const size = change.size;
|
||||||
|
if (this.isFileSizeExceeded(size)) {
|
||||||
|
Logger(`Processing ${change.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.databaseQueuedProcessor.enqueueWithKey(change.path, change);
|
this.databaseQueuedProcessor.enqueueWithKey(change.path, change);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1541,14 +1715,15 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
const chunkCount = collectingChunks.value;
|
const chunkCount = collectingChunks.value;
|
||||||
const pluginScanCount = pluginScanningCount.value;
|
const pluginScanCount = pluginScanningCount.value;
|
||||||
const hiddenFilesCount = hiddenFilesEventCount.value + hiddenFilesProcessingCount.value;
|
const hiddenFilesCount = hiddenFilesEventCount.value + hiddenFilesProcessingCount.value;
|
||||||
|
const conflictProcessCount = this.conflictProcessQueueCount.value;
|
||||||
const labelReplication = replicationCount ? `📥 ${replicationCount} ` : "";
|
const labelReplication = replicationCount ? `📥 ${replicationCount} ` : "";
|
||||||
const labelDBCount = dbCount ? `📄 ${dbCount} ` : "";
|
const labelDBCount = dbCount ? `📄 ${dbCount} ` : "";
|
||||||
const labelStorageCount = storageApplyingCount ? `💾 ${storageApplyingCount}` : "";
|
const labelStorageCount = storageApplyingCount ? `💾 ${storageApplyingCount}` : "";
|
||||||
const labelChunkCount = chunkCount ? `🧩${chunkCount} ` : "";
|
const labelChunkCount = chunkCount ? `🧩${chunkCount} ` : "";
|
||||||
const labelPluginScanCount = pluginScanCount ? `🔌${pluginScanCount} ` : "";
|
const labelPluginScanCount = pluginScanCount ? `🔌${pluginScanCount} ` : "";
|
||||||
const labelHiddenFilesCount = hiddenFilesCount ? `⚙️${hiddenFilesCount} ` : "";
|
const labelHiddenFilesCount = hiddenFilesCount ? `⚙️${hiddenFilesCount} ` : "";
|
||||||
|
const labelConflictProcessCount = conflictProcessCount ? `🔩${conflictProcessCount} ` : "";
|
||||||
return `${labelReplication}${labelDBCount}${labelStorageCount}${labelChunkCount}${labelPluginScanCount}${labelHiddenFilesCount}`;
|
return `${labelReplication}${labelDBCount}${labelStorageCount}${labelChunkCount}${labelPluginScanCount}${labelHiddenFilesCount}${labelConflictProcessCount}`;
|
||||||
})
|
})
|
||||||
|
|
||||||
const replicationStatLabel = reactive(() => {
|
const replicationStatLabel = reactive(() => {
|
||||||
@@ -1625,9 +1800,6 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
|||||||
statusBarLabels.onChanged(applyToDisplay);
|
statusBarLabels.onChanged(applyToDisplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshStatusText() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyStatusBarText(message: string, log: string) {
|
applyStatusBarText(message: string, log: string) {
|
||||||
const newMsg = message;
|
const newMsg = message;
|
||||||
const newLog = log;
|
const newLog = log;
|
||||||
@@ -1759,6 +1931,15 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
return await this.replicator.markRemoteResolved(this.settings);
|
return await this.replicator.markRemoteResolved(this.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFileSizeExceeded(size: number) {
|
||||||
|
if (this.settings.syncMaxSizeInMB > 0 && size > 0) {
|
||||||
|
if (this.settings.syncMaxSizeInMB * 1024 * 1024 < size) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async syncAllFiles(showingNotice?: boolean) {
|
async syncAllFiles(showingNotice?: boolean) {
|
||||||
// synchronize all files between database and storage.
|
// synchronize all files between database and storage.
|
||||||
let initialScan = false;
|
let initialScan = false;
|
||||||
@@ -1809,6 +1990,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
// this.setStatusBarText(`UPDATE DATABASE`);
|
// this.setStatusBarText(`UPDATE DATABASE`);
|
||||||
|
|
||||||
const initProcess = [];
|
const initProcess = [];
|
||||||
|
const logLevel = showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||||
const runAll = async<T>(procedureName: string, objects: T[], callback: (arg: T) => Promise<void>) => {
|
const runAll = async<T>(procedureName: string, objects: T[], callback: (arg: T) => Promise<void>) => {
|
||||||
if (objects.length == 0) {
|
if (objects.length == 0) {
|
||||||
Logger(`${procedureName}: Nothing to do`);
|
Logger(`${procedureName}: Nothing to do`);
|
||||||
@@ -1819,7 +2001,6 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
let success = 0;
|
let success = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
const step = 10;
|
const step = 10;
|
||||||
const logLevel = showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
|
||||||
const processor = new QueueProcessor(async (e) => {
|
const processor = new QueueProcessor(async (e) => {
|
||||||
try {
|
try {
|
||||||
await callback(e[0]);
|
await callback(e[0]);
|
||||||
@@ -1839,15 +2020,24 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
Logger(`${procedureName} All done: DONE:${success}, FAILED:${failed}`, logLevel, `log-${procedureName}`);
|
Logger(`${procedureName} All done: DONE:${success}, FAILED:${failed}`, logLevel, `log-${procedureName}`);
|
||||||
}
|
}
|
||||||
initProcess.push(runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
initProcess.push(runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
||||||
await this.updateIntoDB(e, initialScan);
|
if (!this.isFileSizeExceeded(e.stat.size)) {
|
||||||
|
await this.updateIntoDB(e, initialScan);
|
||||||
|
fireAndForget(() => this.checkAndApplySettingFromMarkdown(e.path, true));
|
||||||
|
} else {
|
||||||
|
Logger(`UPDATE DATABASE: ${e.path} has been skipped due to file size exceeding the limit`, logLevel);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
if (!initialScan) {
|
if (!initialScan) {
|
||||||
initProcess.push(runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
initProcess.push(runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
||||||
const w = await this.localDatabase.getDBEntryMeta(e, {}, true);
|
const w = await this.localDatabase.getDBEntryMeta(e, {}, true);
|
||||||
if (w && !(w.deleted || w._deleted)) {
|
if (w && !(w.deleted || w._deleted)) {
|
||||||
Logger(`Check or pull from db:${e}`);
|
if (!this.isFileSizeExceeded(w.size)) {
|
||||||
await this.pullFile(e, filesStorage, false, null, false);
|
await this.pullFile(e, filesStorage, false, null, false);
|
||||||
Logger(`Check or pull from db:${e} OK`);
|
fireAndForget(() => this.checkAndApplySettingFromMarkdown(e, true));
|
||||||
|
Logger(`Check or pull from db:${e} OK`);
|
||||||
|
} else {
|
||||||
|
Logger(`UPDATE STORAGE: ${e} has been skipped due to file size exceeding the limit`, logLevel);
|
||||||
|
}
|
||||||
} else if (w) {
|
} else if (w) {
|
||||||
Logger(`Deletion history skipped: ${e}`, LOG_LEVEL_VERBOSE);
|
Logger(`Deletion history skipped: ${e}`, LOG_LEVEL_VERBOSE);
|
||||||
} else {
|
} else {
|
||||||
@@ -2147,12 +2337,12 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
* @param path the file location
|
* @param path the file location
|
||||||
* @returns true -> resolved, false -> nothing to do, or check result.
|
* @returns true -> resolved, false -> nothing to do, or check result.
|
||||||
*/
|
*/
|
||||||
async getConflictedStatus(path: FilePathWithPrefix): Promise<diff_check_result> {
|
async checkConflictAndPerformAutoMerge(path: FilePathWithPrefix): Promise<diff_check_result> {
|
||||||
const test = await this.localDatabase.getDBEntry(path, { conflicts: true, revs_info: true }, false, false, true);
|
const test = await this.localDatabase.getDBEntry(path, { conflicts: true, revs_info: true }, false, false, true);
|
||||||
if (test === false) return false;
|
if (test === false) return MISSING_OR_ERROR;
|
||||||
if (test == null) return false;
|
if (test == null) return MISSING_OR_ERROR;
|
||||||
if (!test._conflicts) return false;
|
if (!test._conflicts) return NOT_CONFLICTED;
|
||||||
if (test._conflicts.length == 0) return false;
|
if (test._conflicts.length == 0) return NOT_CONFLICTED;
|
||||||
const conflicts = test._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
const conflicts = test._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
||||||
if ((isSensibleMargeApplicable(path) || isObjectMargeApplicable(path)) && !this.settings.disableMarkdownAutoMerge) {
|
if ((isSensibleMargeApplicable(path) || isObjectMargeApplicable(path)) && !this.settings.disableMarkdownAutoMerge) {
|
||||||
const conflictedRev = conflicts[0];
|
const conflictedRev = conflicts[0];
|
||||||
@@ -2198,7 +2388,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
// ?
|
// ?
|
||||||
await this.pullFile(path);
|
await this.pullFile(path);
|
||||||
Logger(`Automatically merged (sensible) :${path}`, LOG_LEVEL_INFO);
|
Logger(`Automatically merged (sensible) :${path}`, LOG_LEVEL_INFO);
|
||||||
return true;
|
return AUTO_MERGED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2208,14 +2398,14 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
if (leftLeaf == false) {
|
if (leftLeaf == false) {
|
||||||
// what's going on..
|
// what's going on..
|
||||||
Logger(`could not get current revisions:${path}`, LOG_LEVEL_NOTICE);
|
Logger(`could not get current revisions:${path}`, LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return MISSING_OR_ERROR;
|
||||||
}
|
}
|
||||||
if (rightLeaf == false) {
|
if (rightLeaf == false) {
|
||||||
// Conflicted item could not load, delete this.
|
// Conflicted item could not load, delete this.
|
||||||
await this.localDatabase.deleteDBEntry(path, { rev: conflicts[0] });
|
await this.localDatabase.deleteDBEntry(path, { rev: conflicts[0] });
|
||||||
await this.pullFile(path, null, true);
|
await this.pullFile(path, null, true);
|
||||||
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL_NOTICE);
|
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return AUTO_MERGED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
|
const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
|
||||||
@@ -2231,7 +2421,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
|
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
|
||||||
await this.pullFile(path, null, true);
|
await this.pullFile(path, null, true);
|
||||||
Logger(`Automatically merged (${isSame ? "same," : ""}${isBinary ? "binary," : ""}${alwaysNewer ? "alwaysNewer" : ""}) :${path}`, LOG_LEVEL_NOTICE);
|
Logger(`Automatically merged (${isSame ? "same," : ""}${isBinary ? "binary," : ""}${alwaysNewer ? "alwaysNewer" : ""}) :${path}`, LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return AUTO_MERGED;
|
||||||
}
|
}
|
||||||
// make diff.
|
// make diff.
|
||||||
const dmp = new diff_match_patch();
|
const dmp = new diff_match_patch();
|
||||||
@@ -2245,107 +2435,111 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
showMergeDialog(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
|
conflictProcessQueueCount = reactiveSource(0);
|
||||||
return serialized("resolve-conflict:" + filename, () =>
|
conflictResolveQueue =
|
||||||
new Promise((res, rej) => {
|
new KeyedQueueProcessor(async (entries: { filename: FilePathWithPrefix, file: TFile }[]) => {
|
||||||
Logger("open conflict dialog", LOG_LEVEL_VERBOSE);
|
const entry = entries[0];
|
||||||
new ConflictResolveModal(this.app, filename, conflictCheckResult, async (selected) => {
|
const filename = entry.filename;
|
||||||
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, false, true);
|
const conflictCheckResult = await this.checkConflictAndPerformAutoMerge(filename);
|
||||||
if (testDoc === false) {
|
if (conflictCheckResult === MISSING_OR_ERROR || conflictCheckResult === NOT_CONFLICTED || conflictCheckResult === CANCELLED) {
|
||||||
Logger("Missing file..", LOG_LEVEL_VERBOSE);
|
// nothing to do.
|
||||||
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,
|
|
||||||
// delete conflicted revision and write a new file, store it again.
|
|
||||||
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
|
||||||
await this.localDatabase.deleteDBEntry(filename, { rev: testDoc._conflicts[0] });
|
|
||||||
const file = this.vaultAccess.getAbstractFileByPath(stripAllPrefixes(filename)) as TFile;
|
|
||||||
if (file) {
|
|
||||||
if (await this.vaultAccess.vaultModify(file, p)) {
|
|
||||||
await this.updateIntoDB(file);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newFile = await this.vaultAccess.vaultCreate(filename, p);
|
|
||||||
await this.updateIntoDB(newFile);
|
|
||||||
}
|
|
||||||
await this.pullFile(filename);
|
|
||||||
Logger("concat both file");
|
|
||||||
if (this.settings.syncAfterMerge && !this.suspended) {
|
|
||||||
await this.replicate();
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
//resolved, check again.
|
|
||||||
this.showIfConflicted(filename);
|
|
||||||
}, 50);
|
|
||||||
} else if (toDelete == null) {
|
|
||||||
Logger("Leave it still conflicted");
|
|
||||||
} else {
|
|
||||||
await this.localDatabase.deleteDBEntry(filename, { rev: toDelete });
|
|
||||||
await this.pullFile(filename, null, true, toKeep);
|
|
||||||
Logger(`Conflict resolved:${filename}`);
|
|
||||||
if (this.settings.syncAfterMerge && !this.suspended) {
|
|
||||||
await this.replicate();
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
//resolved, check again.
|
|
||||||
this.showIfConflicted(filename);
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res(true);
|
|
||||||
}).open();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
conflictedCheckFiles: FilePath[] = [];
|
|
||||||
|
|
||||||
// queueing the conflicted file check
|
|
||||||
queueConflictedCheck(file: TFile) {
|
|
||||||
this.conflictedCheckFiles = this.conflictedCheckFiles.filter((e) => e != file.path);
|
|
||||||
this.conflictedCheckFiles.push(getPathFromTFile(file));
|
|
||||||
scheduleTask("check-conflict", 100, async () => {
|
|
||||||
const checkFiles = JSON.parse(JSON.stringify(this.conflictedCheckFiles)) as FilePath[];
|
|
||||||
for (const filename of checkFiles) {
|
|
||||||
try {
|
|
||||||
const file = this.vaultAccess.getAbstractFileByPath(filename);
|
|
||||||
if (file != null && file instanceof TFile) {
|
|
||||||
await this.showIfConflicted(getPathFromTFile(file));
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
Logger(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async showIfConflicted(filename: FilePathWithPrefix) {
|
|
||||||
await serialized("conflicted", async () => {
|
|
||||||
const conflictCheckResult = await this.getConflictedStatus(filename);
|
|
||||||
if (conflictCheckResult === false) {
|
|
||||||
//nothing to do.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (conflictCheckResult === true) {
|
if (conflictCheckResult === AUTO_MERGED) {
|
||||||
//auto resolved, but need check again;
|
//auto resolved, but need check again;
|
||||||
if (this.settings.syncAfterMerge && !this.suspended) {
|
if (this.settings.syncAfterMerge && !this.suspended) {
|
||||||
await this.replicate();
|
//Wait for the running replication, if not running replication, run it once.
|
||||||
|
await shareRunningResult(`replication`, () => this.replicate());
|
||||||
}
|
}
|
||||||
Logger("conflict:Automatically merged, but we have to check it again");
|
Logger("conflict:Automatically merged, but we have to check it again");
|
||||||
setTimeout(() => {
|
this.conflictCheckQueue.enqueue(filename);
|
||||||
this.showIfConflicted(filename);
|
|
||||||
}, 50);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//there conflicts, and have to resolve ;
|
if (this.settings.showMergeDialogOnlyOnActive) {
|
||||||
await this.showMergeDialog(filename, conflictCheckResult);
|
const af = this.app.workspace.getActiveFile();
|
||||||
|
if (af && af.path != filename) {
|
||||||
|
Logger(`${filename} is conflicted. Merging process has been postponed to the file have got opened.`, LOG_LEVEL_NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger("conflict:Manual merge required!");
|
||||||
|
await this.resolveConflictByUI(filename, conflictCheckResult);
|
||||||
|
}, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false }).replaceEnqueueProcessor(
|
||||||
|
(queue, newEntity) => {
|
||||||
|
const filename = newEntity.entity.filename;
|
||||||
|
sendValue("cancel-resolve-conflict:" + filename, true);
|
||||||
|
const newQueue = [...queue].filter(e => e.key != newEntity.key);
|
||||||
|
return [...newQueue, newEntity];
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
conflictCheckQueue =
|
||||||
|
// First process - Check is the file actually need resolve -
|
||||||
|
new QueueProcessor((files: FilePathWithPrefix[]) => {
|
||||||
|
const filename = files[0];
|
||||||
|
const file = this.vaultAccess.getAbstractFileByPath(filename);
|
||||||
|
if (!file) return;
|
||||||
|
if (!(file instanceof TFile)) return;
|
||||||
|
// Check again?
|
||||||
|
|
||||||
|
return [{ key: filename, entity: { filename, file } }];
|
||||||
|
// this.conflictResolveQueue.enqueueWithKey(filename, { filename, file });
|
||||||
|
}, {
|
||||||
|
suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, pipeTo: this.conflictResolveQueue, totalRemainingReactiveSource: this.conflictProcessQueueCount
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async resolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
|
||||||
|
Logger("Merge:open conflict dialog", LOG_LEVEL_VERBOSE);
|
||||||
|
const dialog = new ConflictResolveModal(this.app, filename, conflictCheckResult);
|
||||||
|
dialog.open();
|
||||||
|
const selected = await dialog.waitForResult();
|
||||||
|
if (selected === CANCELLED) {
|
||||||
|
// Cancelled by UI, or another conflict.
|
||||||
|
Logger(`Merge: Cancelled ${filename}`, LOG_LEVEL_INFO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, false, true);
|
||||||
|
if (testDoc === false) {
|
||||||
|
Logger(`Merge: Could not read ${filename} from the local database`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!testDoc._conflicts) {
|
||||||
|
Logger(`Merge: Nothing to do ${filename}`, LOG_LEVEL_VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toDelete = selected;
|
||||||
|
const toKeep = conflictCheckResult.left.rev != toDelete ? conflictCheckResult.left.rev : conflictCheckResult.right.rev;
|
||||||
|
if (toDelete === LEAVE_TO_SUBSEQUENT) {
|
||||||
|
// concat both,
|
||||||
|
// delete conflicted revision and write a new file, store it again.
|
||||||
|
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
||||||
|
await this.localDatabase.deleteDBEntry(filename, { rev: testDoc._conflicts[0] });
|
||||||
|
const file = this.vaultAccess.getAbstractFileByPath(stripAllPrefixes(filename)) as TFile;
|
||||||
|
if (file) {
|
||||||
|
if (await this.vaultAccess.vaultModify(file, p)) {
|
||||||
|
await this.updateIntoDB(file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newFile = await this.vaultAccess.vaultCreate(filename, p);
|
||||||
|
await this.updateIntoDB(newFile);
|
||||||
|
}
|
||||||
|
await this.pullFile(filename);
|
||||||
|
Logger(`Merge: Changes has been concatenated: ${filename}`);
|
||||||
|
} else if (typeof toDelete === "string") {
|
||||||
|
await this.localDatabase.deleteDBEntry(filename, { rev: toDelete });
|
||||||
|
await this.pullFile(filename, null, true, toKeep);
|
||||||
|
Logger(`Conflict resolved:${filename}`);
|
||||||
|
} else {
|
||||||
|
Logger(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// In here, some merge has been processed.
|
||||||
|
// So we have to run replication if configured.
|
||||||
|
if (this.settings.syncAfterMerge && !this.suspended) {
|
||||||
|
await shareRunningResult(`replication`, () => this.replicate());
|
||||||
|
}
|
||||||
|
// And, check it again.
|
||||||
|
this.conflictCheckQueue.enqueue(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pullFile(filename: FilePathWithPrefix, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
async pullFile(filename: FilePathWithPrefix, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
||||||
@@ -2358,7 +2552,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
Logger(`${filename} Skipped`);
|
Logger(`${filename} Skipped`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.doc2storage(doc, undefined, force);
|
await this.processEntryDoc(doc, undefined, force);
|
||||||
} else if (targetFile instanceof TFile) {
|
} else if (targetFile instanceof TFile) {
|
||||||
//normal case
|
//normal case
|
||||||
const file = targetFile;
|
const file = targetFile;
|
||||||
@@ -2367,7 +2561,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
Logger(`${filename} Skipped`);
|
Logger(`${filename} Skipped`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.doc2storage(doc, file, force);
|
await this.processEntryDoc(doc, file, force);
|
||||||
} else {
|
} else {
|
||||||
Logger(`target files:${filename} is exists as the folder`);
|
Logger(`target files:${filename} is exists as the folder`);
|
||||||
//something went wrong..
|
//something went wrong..
|
||||||
@@ -2399,23 +2593,32 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
}
|
}
|
||||||
if (storageMtime > docMtime) {
|
if (storageMtime > docMtime) {
|
||||||
//newer local file.
|
//newer local file.
|
||||||
Logger("STORAGE -> DB :" + file.path);
|
if (!this.isFileSizeExceeded(file.stat.size)) {
|
||||||
Logger(`${storageMtime} > ${docMtime}`);
|
Logger("STORAGE -> DB :" + file.path);
|
||||||
await this.updateIntoDB(file, initialScan);
|
Logger(`${storageMtime} > ${docMtime}`);
|
||||||
caches[dK] = { storageMtime, docMtime };
|
await this.updateIntoDB(file, initialScan);
|
||||||
return caches;
|
fireAndForget(() => this.checkAndApplySettingFromMarkdown(file.path, true));
|
||||||
|
caches[dK] = { storageMtime, docMtime };
|
||||||
|
return caches;
|
||||||
|
} else {
|
||||||
|
Logger(`STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
|
||||||
|
}
|
||||||
} else if (storageMtime < docMtime) {
|
} else if (storageMtime < docMtime) {
|
||||||
//newer database file.
|
//newer database file.
|
||||||
Logger("STORAGE <- DB :" + file.path);
|
if (!this.isFileSizeExceeded(doc.size)) {
|
||||||
Logger(`${storageMtime} < ${docMtime}`);
|
Logger("STORAGE <- DB :" + file.path);
|
||||||
const docx = await this.localDatabase.getDBEntry(getPathFromTFile(file), null, false, false);
|
Logger(`${storageMtime} < ${docMtime}`);
|
||||||
if (docx != false) {
|
const docx = await this.localDatabase.getDBEntry(getPathFromTFile(file), null, false, false);
|
||||||
await this.doc2storage(docx, file);
|
if (docx != false) {
|
||||||
|
await this.processEntryDoc(docx, file);
|
||||||
|
} else {
|
||||||
|
Logger(`STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
|
||||||
|
}
|
||||||
|
caches[dK] = { storageMtime, docMtime };
|
||||||
|
return caches;
|
||||||
} else {
|
} else {
|
||||||
Logger("STORAGE <- DB :" + file.path + " Skipped");
|
Logger("STORAGE <- DB :" + file.path + " Skipped (size)");
|
||||||
}
|
}
|
||||||
caches[dK] = { storageMtime, docMtime };
|
|
||||||
return caches;
|
|
||||||
}
|
}
|
||||||
Logger("STORAGE == DB :" + file.path + "", LOG_LEVEL_VERBOSE);
|
Logger("STORAGE == DB :" + file.path + "", LOG_LEVEL_VERBOSE);
|
||||||
caches[dK] = { storageMtime, docMtime };
|
caches[dK] = { storageMtime, docMtime };
|
||||||
|
|||||||
22
src/utils.ts
22
src/utils.ts
@@ -1,12 +1,13 @@
|
|||||||
import { normalizePath, Platform, TAbstractFile, App, Plugin, type RequestUrlParam, requestUrl } from "./deps";
|
import { normalizePath, Platform, TAbstractFile, App, type RequestUrlParam, requestUrl } from "./deps";
|
||||||
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "./lib/src/path";
|
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "./lib/src/path";
|
||||||
|
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
|
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
|
||||||
import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types";
|
import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types";
|
||||||
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import type ObsidianLiveSyncPlugin from "./main";
|
||||||
import { writeString } from "./lib/src/strbin";
|
import { writeString } from "./lib/src/strbin";
|
||||||
|
import { fireAndForget } from "./lib/src/utils";
|
||||||
|
|
||||||
export { scheduleTask, setPeriodicTask, cancelTask, cancelAllTasks, cancelPeriodicTask, cancelAllPeriodicTask, } from "./lib/src/task";
|
export { scheduleTask, setPeriodicTask, cancelTask, cancelAllTasks, cancelPeriodicTask, cancelAllPeriodicTask, } from "./lib/src/task";
|
||||||
|
|
||||||
@@ -336,8 +337,8 @@ export const askString = (app: App, title: string, key: string, placeholder: str
|
|||||||
export class PeriodicProcessor {
|
export class PeriodicProcessor {
|
||||||
_process: () => Promise<any>;
|
_process: () => Promise<any>;
|
||||||
_timer?: number;
|
_timer?: number;
|
||||||
_plugin: Plugin;
|
_plugin: ObsidianLiveSyncPlugin;
|
||||||
constructor(plugin: Plugin, process: () => Promise<any>) {
|
constructor(plugin: ObsidianLiveSyncPlugin, process: () => Promise<any>) {
|
||||||
this._plugin = plugin;
|
this._plugin = plugin;
|
||||||
this._process = process;
|
this._process = process;
|
||||||
}
|
}
|
||||||
@@ -351,12 +352,19 @@ export class PeriodicProcessor {
|
|||||||
enable(interval: number) {
|
enable(interval: number) {
|
||||||
this.disable();
|
this.disable();
|
||||||
if (interval == 0) return;
|
if (interval == 0) return;
|
||||||
this._timer = window.setInterval(() => this.process().then(() => { }), interval);
|
this._timer = window.setInterval(() => fireAndForget(async () => {
|
||||||
|
await this.process();
|
||||||
|
if (this._plugin._unloaded) {
|
||||||
|
this.disable();
|
||||||
|
}
|
||||||
|
}), interval);
|
||||||
this._plugin.registerInterval(this._timer);
|
this._plugin.registerInterval(this._timer);
|
||||||
}
|
}
|
||||||
disable() {
|
disable() {
|
||||||
if (this._timer !== undefined) window.clearInterval(this._timer);
|
if (this._timer !== undefined) {
|
||||||
this._timer = undefined;
|
window.clearInterval(this._timer);
|
||||||
|
this._timer = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
50
styles.css
50
styles.css
@@ -85,7 +85,6 @@
|
|||||||
|
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
|
||||||
.sls-header-button {
|
.sls-header-button {
|
||||||
margin-left: 2em;
|
margin-left: 2em;
|
||||||
}
|
}
|
||||||
@@ -99,7 +98,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-wrap::before,
|
.CodeMirror-wrap::before,
|
||||||
.cm-s-obsidian>.cm-editor::before,
|
.cm-s-obsidian > .cm-editor::before,
|
||||||
.canvas-wrapper::before {
|
.canvas-wrapper::before {
|
||||||
content: attr(data-log);
|
content: attr(data-log);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -124,7 +123,7 @@
|
|||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-s-obsidian>.cm-editor::before {
|
.cm-s-obsidian > .cm-editor::before {
|
||||||
right: 16px;
|
right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,8 +152,8 @@ div.sls-setting-menu-btn {
|
|||||||
/* width: 100%; */
|
/* width: 100%; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-setting-tab:hover~div.sls-setting-menu-btn,
|
.sls-setting-tab:hover ~ div.sls-setting-menu-btn,
|
||||||
.sls-setting-label.selected .sls-setting-tab:checked~div.sls-setting-menu-btn {
|
.sls-setting-label.selected .sls-setting-tab:checked ~ div.sls-setting-menu-btn {
|
||||||
background-color: var(--interactive-accent);
|
background-color: var(--interactive-accent);
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
@@ -257,8 +256,8 @@ div.sls-setting-menu-btn {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-input > .setting-item-control >input {
|
.password-input > .setting-item-control > input {
|
||||||
-webkit-text-security: disc;
|
-webkit-text-security: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.ls-mark-cr::after {
|
span.ls-mark-cr::after {
|
||||||
@@ -270,4 +269,39 @@ span.ls-mark-cr::after {
|
|||||||
|
|
||||||
.deleted span.ls-mark-cr::after {
|
.deleted span.ls-mark-cr::after {
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ls-imgdiff-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ls-imgdiff-wrap .overlay {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ls-imgdiff-wrap .overlay .img-base {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.ls-imgdiff-wrap .overlay .img-overlay {
|
||||||
|
-webkit-filter: invert(100%) opacity(50%);
|
||||||
|
filter: invert(100%) opacity(50%);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
animation: ls-blink-diff 0.5s cubic-bezier(0.4, 0, 1, 1) infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes ls-blink-diff {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
26
updates.md
26
updates.md
@@ -7,8 +7,34 @@ Of course, I think this would be our suffering in some cases. However, I would l
|
|||||||
Sorry for being absent so much long. And thank you for your patience!
|
Sorry for being absent so much long. And thank you for your patience!
|
||||||
|
|
||||||
Note: we got a very performance improvement.
|
Note: we got a very performance improvement.
|
||||||
|
Note at 0.22.2: **Now, to rescue mobile devices, Maximum file size is set to 50 by default**. Please configure the limit as you need. If you do not want to limit the sizes, set zero manually, please.
|
||||||
|
|
||||||
#### Version history
|
#### Version history
|
||||||
|
- 0.22.2
|
||||||
|
- Fixed:
|
||||||
|
- Now the results of resolving conflicts are surely synchronised.
|
||||||
|
- Modified:
|
||||||
|
- Some setting items got new clear names. (`Sync Settings` -> `Targets`).
|
||||||
|
- New feature:
|
||||||
|
- We can limit the synchronising files by their size. (`Sync Settings` -> `Targets` -> `Maximum file size`).
|
||||||
|
- It depends on the size of the newer one.
|
||||||
|
- At Obsidian 1.5.3 on mobile, we should set this to around 50MB to avoid restarting Obsidian.
|
||||||
|
- Now the settings could be stored in a specific markdown file to synchronise or switch it (`General Setting` -> `Share settings via markdown`).
|
||||||
|
- [Screwdriver](https://github.com/vrtmrz/obsidian-screwdriver) is quite good, but mostly we only need this.
|
||||||
|
- Customisation of the obsoleted device is now able to be deleted at once.
|
||||||
|
- We have to put the maintenance mode in at the Customisation sync dialogue.
|
||||||
|
- 0.22.1
|
||||||
|
- New feature:
|
||||||
|
- We can perform automatic conflict resolution for inactive files, and postpone only manual ones by `Postpone manual resolution of inactive files`.
|
||||||
|
- Now we can see the image in the document history dialogue.
|
||||||
|
- We can see the difference of the image, in the document history dialogue.
|
||||||
|
- And also we can highlight differences.
|
||||||
|
- Improved:
|
||||||
|
- Hidden file sync has been stabilised.
|
||||||
|
- Now automatically reloads the conflict-resolution dialogue when new conflicted revisions have arrived.
|
||||||
|
- Fixed:
|
||||||
|
- No longer periodic process runs after unloading the plug-in.
|
||||||
|
- Now the modification of binary files is surely stored in the storage.
|
||||||
- 0.22.0
|
- 0.22.0
|
||||||
- Refined:
|
- Refined:
|
||||||
- Task scheduling logics has been rewritten.
|
- Task scheduling logics has been rewritten.
|
||||||
|
|||||||
Reference in New Issue
Block a user