mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-13 17:55:56 +00:00
Fixed:
- Now the filename is shown on the Conflict resolving dialog - Rename of files has been improved again.
This commit is contained in:
@@ -6,12 +6,14 @@ import { escapeStringToHTML } from "./lib/src/strbin";
|
|||||||
export class ConflictResolveModal extends Modal {
|
export class ConflictResolveModal extends Modal {
|
||||||
// result: Array<[number, string]>;
|
// result: Array<[number, string]>;
|
||||||
result: diff_result;
|
result: diff_result;
|
||||||
|
filename: string;
|
||||||
callback: (remove_rev: string) => Promise<void>;
|
callback: (remove_rev: string) => Promise<void>;
|
||||||
|
|
||||||
constructor(app: App, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
|
constructor(app: App, filename: string, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
|
||||||
super(app);
|
super(app);
|
||||||
this.result = diff;
|
this.result = diff;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
this.filename = filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
@@ -20,6 +22,7 @@ export class ConflictResolveModal extends Modal {
|
|||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
|
|
||||||
contentEl.createEl("h2", { text: "This document has conflicted changes." });
|
contentEl.createEl("h2", { text: "This document has conflicted changes." });
|
||||||
|
contentEl.createEl("span", this.filename);
|
||||||
const div = contentEl.createDiv("");
|
const div = contentEl.createDiv("");
|
||||||
div.addClass("op-scrollable");
|
div.addClass("op-scrollable");
|
||||||
let diff = "";
|
let diff = "";
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 133bae3607...7be1dad0be
134
src/main.ts
134
src/main.ts
@@ -1,7 +1,7 @@
|
|||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App } from "obsidian";
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App } from "obsidian";
|
||||||
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry } from "./lib/src/types";
|
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry } from "./lib/src/types";
|
||||||
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo, queueItem } from "./types";
|
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo, queueItem, FileInfo } from "./types";
|
||||||
import { getDocData, isDocContentSame } from "./lib/src/utils";
|
import { getDocData, isDocContentSame } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { LocalPouchDB } from "./LocalPouchDB";
|
import { LocalPouchDB } from "./LocalPouchDB";
|
||||||
@@ -112,14 +112,15 @@ function clearTouched() {
|
|||||||
type CacheData = string | ArrayBuffer;
|
type CacheData = string | ArrayBuffer;
|
||||||
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
||||||
type FileEventArgs = {
|
type FileEventArgs = {
|
||||||
file: TAbstractFile | InternalFileInfo;
|
file: FileInfo | InternalFileInfo;
|
||||||
cache?: CacheData;
|
cache?: CacheData;
|
||||||
oldPath?: string;
|
oldPath?: string;
|
||||||
ctx?: any;
|
ctx?: any;
|
||||||
}
|
}
|
||||||
type FileEventItem = {
|
type FileEventItem = {
|
||||||
type: FileEventType,
|
type: FileEventType,
|
||||||
args: FileEventArgs
|
args: FileEventArgs,
|
||||||
|
key: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||||
@@ -815,16 +816,22 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache file and waiting to can be proceed.
|
// Cache file and waiting to can be proceed.
|
||||||
async appendWatchEvent(type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string, ctx?: any) {
|
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
||||||
// check really we can process.
|
let forcePerform = false;
|
||||||
if (file instanceof TFile && !this.isTargetFile(file)) return;
|
for (const param of params) {
|
||||||
if (this.settings.suspendFileWatching) return;
|
const atomicKey = [0, 0, 0, 0, 0, 0].map(e => `${Math.floor(Math.random() * 100000)}`).join("-");
|
||||||
|
const type = param.type;
|
||||||
|
const file = param.file;
|
||||||
|
const oldPath = param.oldPath;
|
||||||
|
if (file instanceof TFolder) continue;
|
||||||
|
if (!this.isTargetFile(file.path)) continue;
|
||||||
|
if (this.settings.suspendFileWatching) continue;
|
||||||
|
|
||||||
let cache: null | string | ArrayBuffer;
|
let cache: null | string | ArrayBuffer;
|
||||||
// new file or something changed, cache the changes.
|
// new file or something changed, cache the changes.
|
||||||
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
|
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
|
||||||
if (recentlyTouched(file)) {
|
if (recentlyTouched(file)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
if (!isPlainText(file.name)) {
|
if (!isPlainText(file.name)) {
|
||||||
cache = await this.app.vault.readBinary(file);
|
cache = await this.app.vault.readBinary(file);
|
||||||
@@ -834,6 +841,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (!cache) cache = await this.app.vault.read(file);
|
if (!cache) cache = await this.app.vault.read(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (type == "DELETE" || type == "RENAME") {
|
||||||
|
forcePerform = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (this.settings.batchSave) {
|
if (this.settings.batchSave) {
|
||||||
@@ -844,30 +854,41 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
// a.md CREATE
|
// a.md CREATE
|
||||||
// :
|
// :
|
||||||
let i = this.watchedFileEventQueue.length;
|
let i = this.watchedFileEventQueue.length;
|
||||||
|
L1:
|
||||||
while (i >= 0) {
|
while (i >= 0) {
|
||||||
i--;
|
i--;
|
||||||
if (i < 0) break;
|
if (i < 0) break L1;
|
||||||
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
|
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
|
||||||
continue;
|
continue L1;
|
||||||
}
|
}
|
||||||
if (this.watchedFileEventQueue[i].type != type) break;
|
if (this.watchedFileEventQueue[i].type != type) break L1;
|
||||||
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
||||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileInfo = file instanceof TFile ? {
|
||||||
|
ctime: file.stat.ctime,
|
||||||
|
mtime: file.stat.mtime,
|
||||||
|
file: file,
|
||||||
|
path: file.path,
|
||||||
|
size: file.stat.size
|
||||||
|
} as FileInfo : file as InternalFileInfo;
|
||||||
this.watchedFileEventQueue.push({
|
this.watchedFileEventQueue.push({
|
||||||
type,
|
type,
|
||||||
args: {
|
args: {
|
||||||
file,
|
file: fileInfo,
|
||||||
oldPath,
|
oldPath,
|
||||||
cache,
|
cache,
|
||||||
ctx
|
ctx
|
||||||
}
|
},
|
||||||
|
key: atomicKey
|
||||||
})
|
})
|
||||||
|
}
|
||||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||||
|
console.dir([...this.watchedFileEventQueue]);
|
||||||
if (this.isReady) {
|
if (this.isReady) {
|
||||||
await this.procFileEvent();
|
await this.procFileEvent(forcePerform);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -885,39 +906,52 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
clearTrigger("applyBatchAuto");
|
clearTrigger("applyBatchAuto");
|
||||||
const ret = await runWithLock("procFiles", true, async () => {
|
const ret = await runWithLock("procFiles", true, async () => {
|
||||||
|
L2:
|
||||||
do {
|
do {
|
||||||
const procs = [...this.watchedFileEventQueue];
|
const procs = [...this.watchedFileEventQueue];
|
||||||
this.watchedFileEventQueue = [];
|
this.watchedFileEventQueue = [];
|
||||||
for (const queue of procs) {
|
|
||||||
|
L1:
|
||||||
|
do {
|
||||||
|
const queue = procs.shift();
|
||||||
|
if (queue === undefined) break L1;
|
||||||
|
console.warn([queue.type, { ...queue.args, cache: undefined }]);
|
||||||
|
|
||||||
const file = queue.args.file;
|
const file = queue.args.file;
|
||||||
const key = `file-last-proc-${queue.type}-${file.path}`;
|
const key = `file-last-proc-${queue.type}-${file.path}`;
|
||||||
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
|
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
|
||||||
if (file instanceof TFile && file.stat.mtime == last) {
|
if (queue.type == "DELETE") {
|
||||||
|
await this.deleteFromDBbyPath(file.path);
|
||||||
|
} else if (queue.type == "INTERNAL") {
|
||||||
|
await this.watchVaultRawEventsAsync(file.path);
|
||||||
|
} else {
|
||||||
|
const targetFile = this.app.vault.getAbstractFileByPath(file.path);
|
||||||
|
if (!(targetFile instanceof TFile)) {
|
||||||
|
Logger(`Target file was not found: ${file.path}`, LOG_LEVEL.INFO);
|
||||||
|
continue L1;
|
||||||
|
}
|
||||||
|
//TODO: check from cache time.
|
||||||
|
if (file.mtime == last) {
|
||||||
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
|
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||||
continue;
|
continue L1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = queue.args.cache;
|
const cache = queue.args.cache;
|
||||||
if ((queue.type == "CREATE" || queue.type == "CHANGED") && file instanceof TFile) {
|
if (queue.type == "CREATE" || queue.type == "CHANGED") {
|
||||||
await this.updateIntoDB(file, false, cache);
|
if (!await this.updateIntoDB(targetFile, false, cache)) {
|
||||||
}
|
Logger(`DB -> STORAGE: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL.INFO);
|
||||||
if (queue.type == "DELETE") {
|
// cancel running queues and remove one of atomic operation
|
||||||
if (file instanceof TFile) {
|
this.watchedFileEventQueue = [...procs, ...this.watchedFileEventQueue].filter(e => e.key != queue.key);
|
||||||
await this.deleteFromDB(file);
|
continue L2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (queue.type == "RENAME") {
|
if (queue.type == "RENAME") {
|
||||||
if (file instanceof TFile) {
|
// Obsolete
|
||||||
await this.watchVaultRenameAsync(file, queue.args.oldPath);
|
await this.watchVaultRenameAsync(targetFile, queue.args.oldPath);
|
||||||
}
|
|
||||||
}
|
|
||||||
if (queue.type == "INTERNAL") {
|
|
||||||
await this.watchVaultRawEventsAsync(file.path);
|
|
||||||
}
|
|
||||||
if (file instanceof TFile) {
|
|
||||||
await this.localDatabase.kvDB.set(key, file.stat.mtime);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await this.localDatabase.kvDB.set(key, file.mtime);
|
||||||
|
} while (procs.length > 0);
|
||||||
} while (this.watchedFileEventQueue.length != 0);
|
} while (this.watchedFileEventQueue.length != 0);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
@@ -925,18 +959,23 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||||
this.appendWatchEvent("CREATE", file, null, ctx);
|
this.appendWatchEvent([{ type: "CREATE", file }], ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||||
this.appendWatchEvent("CHANGED", file, null, ctx);
|
this.appendWatchEvent([{ type: "CHANGED", file }], ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||||
this.appendWatchEvent("DELETE", file, null, ctx);
|
this.appendWatchEvent([{ type: "DELETE", file }], ctx);
|
||||||
}
|
}
|
||||||
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
||||||
this.appendWatchEvent("RENAME", file, oldFile, ctx);
|
if (file instanceof TFile) {
|
||||||
|
this.appendWatchEvent([
|
||||||
|
{ type: "CREATE", file },
|
||||||
|
{ type: "DELETE", file: { path: oldFile, mtime: file.stat.mtime, ctime: file.stat.ctime, size: file.stat.size, deleted: true } }
|
||||||
|
], ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watchWorkspaceOpen(file: TFile) {
|
watchWorkspaceOpen(file: TFile) {
|
||||||
@@ -973,7 +1012,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
.replace(/\n| /g, "")
|
.replace(/\n| /g, "")
|
||||||
.split(",").filter(e => e).map(e => new RegExp(e));
|
.split(",").filter(e => e).map(e => new RegExp(e));
|
||||||
if (ignorePatterns.some(e => path.match(e))) return;
|
if (ignorePatterns.some(e => path.match(e))) return;
|
||||||
this.appendWatchEvent("INTERNAL", { path, mtime: 0, ctime: 0, size: 0 }, "", null);
|
this.appendWatchEvent(
|
||||||
|
[{
|
||||||
|
type: "INTERNAL",
|
||||||
|
file: { path, mtime: 0, ctime: 0, size: 0 }
|
||||||
|
}], null);
|
||||||
}
|
}
|
||||||
recentProcessedInternalFiles = [] as string[];
|
recentProcessedInternalFiles = [] as string[];
|
||||||
async watchVaultRawEventsAsync(path: string) {
|
async watchVaultRawEventsAsync(path: string) {
|
||||||
@@ -1040,9 +1083,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (file instanceof TFile) {
|
if (file instanceof TFile) {
|
||||||
try {
|
try {
|
||||||
// Logger(`RENAMING.. ${file.path} into db`);
|
// Logger(`RENAMING.. ${file.path} into db`);
|
||||||
await this.updateIntoDB(file, false, cache);
|
if (await this.updateIntoDB(file, false, cache)) {
|
||||||
// Logger(`deleted ${oldFile} from db`);
|
// Logger(`deleted ${oldFile} from db`);
|
||||||
await this.deleteFromDBbyPath(oldFile);
|
await this.deleteFromDBbyPath(oldFile);
|
||||||
|
} else {
|
||||||
|
Logger(`Could not save new file: ${file.path} `, LOG_LEVEL.NOTICE);
|
||||||
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
}
|
}
|
||||||
@@ -2240,7 +2286,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
showMergeDialog(filename: string, conflictCheckResult: diff_result): Promise<boolean> {
|
showMergeDialog(filename: string, conflictCheckResult: diff_result): Promise<boolean> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
|
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
|
||||||
new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => {
|
new ConflictResolveModal(this.app, filename, conflictCheckResult, async (selected) => {
|
||||||
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, false, true);
|
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, false, true);
|
||||||
if (testDoc === false) {
|
if (testDoc === false) {
|
||||||
Logger("Missing file..", LOG_LEVEL.VERBOSE);
|
Logger("Missing file..", LOG_LEVEL.VERBOSE);
|
||||||
@@ -2422,9 +2468,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateIntoDB(file: TFile, initialScan?: boolean, cache?: CacheData, force?: boolean) {
|
async updateIntoDB(file: TFile, initialScan?: boolean, cache?: CacheData, force?: boolean) {
|
||||||
if (!this.isTargetFile(file)) return;
|
if (!this.isTargetFile(file)) return true;
|
||||||
if (shouldBeIgnored(file.path)) {
|
if (shouldBeIgnored(file.path)) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
let content: string | string[];
|
let content: string | string[];
|
||||||
let datatype: "plain" | "newnote" = "newnote";
|
let datatype: "plain" | "newnote" = "newnote";
|
||||||
@@ -2486,15 +2532,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
if (isNotChanged) return;
|
if (isNotChanged) return true;
|
||||||
await this.localDatabase.putDBEntry(d, initialScan);
|
const ret = await this.localDatabase.putDBEntry(d, initialScan);
|
||||||
this.queuedFiles = this.queuedFiles.map((e) => ({ ...e, ...(e.entry._id == d._id ? { done: true } : {}) }));
|
this.queuedFiles = this.queuedFiles.map((e) => ({ ...e, ...(e.entry._id == d._id ? { done: true } : {}) }));
|
||||||
|
|
||||||
|
|
||||||
Logger(msg + fullPath);
|
Logger(msg + fullPath);
|
||||||
if (this.settings.syncOnSave && !this.suspended) {
|
if (this.settings.syncOnSave && !this.suspended) {
|
||||||
await this.replicate();
|
await this.replicate();
|
||||||
}
|
}
|
||||||
|
return ret != false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFromDB(file: TFile) {
|
async deleteFromDB(file: TFile) {
|
||||||
|
|||||||
11
src/types.ts
11
src/types.ts
@@ -1,4 +1,4 @@
|
|||||||
import { PluginManifest } from "obsidian";
|
import { PluginManifest, TFile } from "obsidian";
|
||||||
import { DatabaseEntry, EntryBody } from "./lib/src/types";
|
import { DatabaseEntry, EntryBody } from "./lib/src/types";
|
||||||
|
|
||||||
export interface PluginDataEntry extends DatabaseEntry {
|
export interface PluginDataEntry extends DatabaseEntry {
|
||||||
@@ -31,6 +31,15 @@ export interface InternalFileInfo {
|
|||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
path: string;
|
||||||
|
mtime: number;
|
||||||
|
ctime: number;
|
||||||
|
size: number;
|
||||||
|
deleted?: boolean;
|
||||||
|
file: TFile;
|
||||||
|
}
|
||||||
|
|
||||||
export type queueItem = {
|
export type queueItem = {
|
||||||
entry: EntryBody;
|
entry: EntryBody;
|
||||||
missingChildren: string[];
|
missingChildren: string[];
|
||||||
|
|||||||
Reference in New Issue
Block a user