mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-15 18:55:57 +00:00
Fixed:
- File tracking logic has been refined.
This commit is contained in:
304
src/main.ts
304
src/main.ts
@@ -41,7 +41,7 @@ setNoticeClass(Notice);
|
||||
const ICHeader = "i:";
|
||||
const ICHeaderEnd = "i;";
|
||||
const ICHeaderLength = ICHeader.length;
|
||||
|
||||
const FileWatchEventQueueMax = 10;
|
||||
|
||||
/**
|
||||
* returns is internal chunk of file
|
||||
@@ -109,6 +109,19 @@ function recentlyTouched(file: TFile) {
|
||||
function clearTouched() {
|
||||
touchedFiles = [];
|
||||
}
|
||||
|
||||
type CacheData = string | ArrayBuffer;
|
||||
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME";
|
||||
type FileEventArgs = {
|
||||
file: TAbstractFile;
|
||||
cache?: CacheData;
|
||||
oldPath?: string;
|
||||
ctx?: any;
|
||||
}
|
||||
type FileEventItem = {
|
||||
type: FileEventType,
|
||||
args: FileEventArgs
|
||||
}
|
||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
settings: ObsidianLiveSyncSettings;
|
||||
localDatabase: LocalPouchDB;
|
||||
@@ -118,6 +131,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
suspended: boolean;
|
||||
deviceAndVaultName: string;
|
||||
isMobile = false;
|
||||
isReady = false;
|
||||
|
||||
watchedFileEventQueue = [] as FileEventItem[];
|
||||
|
||||
getVaultName(): string {
|
||||
return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
|
||||
@@ -277,10 +293,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.refreshStatusText = this.refreshStatusText.bind(this);
|
||||
|
||||
this.statusBar2 = this.addStatusBarItem();
|
||||
// this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false);
|
||||
// this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false);
|
||||
// this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false);
|
||||
|
||||
this.watchVaultChange = this.watchVaultChange.bind(this);
|
||||
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||
@@ -298,6 +310,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
// this.registerWatchEvents();
|
||||
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
||||
|
||||
this.registerFileWatchEvents();
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
if (this.localDatabase.isReady)
|
||||
try {
|
||||
@@ -700,12 +713,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
gcTimerHandler: any = null;
|
||||
|
||||
|
||||
registerWatchEvents() {
|
||||
registerFileWatchEvents() {
|
||||
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
|
||||
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
||||
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
||||
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
|
||||
}
|
||||
|
||||
registerWatchEvents() {
|
||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||
window.addEventListener("visibilitychange", this.watchWindowVisibility);
|
||||
window.addEventListener("online", this.watchOnline);
|
||||
@@ -728,6 +743,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
async watchWindowVisibilityAsync() {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (!this.isReady) return;
|
||||
// if (this.suspended) return;
|
||||
const isHidden = document.hidden;
|
||||
await this.applyBatchChange();
|
||||
@@ -752,12 +768,125 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache file and waiting to can be proceed.
|
||||
async appendWatchEvent(type: FileEventType, file: TAbstractFile, oldPath?: string, ctx?: any) {
|
||||
// check really we can process.
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
|
||||
let cache: null | string | ArrayBuffer;
|
||||
// new file or something changed, cache the changes.
|
||||
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
|
||||
if (recentlyTouched(file)) {
|
||||
return;
|
||||
}
|
||||
if (!isPlainText(file.name)) {
|
||||
cache = await this.app.vault.readBinary(file);
|
||||
} else {
|
||||
// cache = await this.app.vault.read(file);
|
||||
cache = await this.app.vault.cachedRead(file);
|
||||
if (!cache) cache = await this.app.vault.read(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.settings.batchSave) {
|
||||
// if the latest event is the same type, omit that
|
||||
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
|
||||
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
|
||||
// a.md MODIFY
|
||||
// a.md CREATE
|
||||
// :
|
||||
let i = this.watchedFileEventQueue.length;
|
||||
while (i >= 0) {
|
||||
i--;
|
||||
if (i < 0) break;
|
||||
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
|
||||
continue;
|
||||
}
|
||||
if (this.watchedFileEventQueue[i].type != type) break;
|
||||
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this.watchedFileEventQueue.push({
|
||||
type,
|
||||
args: {
|
||||
file,
|
||||
oldPath,
|
||||
cache,
|
||||
ctx
|
||||
}
|
||||
})
|
||||
this.refreshStatusText();
|
||||
if (this.isReady) {
|
||||
await this.procFileEvent();
|
||||
}
|
||||
|
||||
}
|
||||
async procFileEvent(applyBatch?: boolean) {
|
||||
if (!this.isReady) return;
|
||||
if (this.settings.batchSave) {
|
||||
if (!applyBatch && this.watchedFileEventQueue.length < FileWatchEventQueueMax) {
|
||||
// Defer till applying batch save or queue has been grown enough.
|
||||
// or 120 seconds after.
|
||||
setTrigger("applyBatchAuto", 120000, () => {
|
||||
this.procFileEvent(true);
|
||||
})
|
||||
return;
|
||||
}
|
||||
}
|
||||
clearTrigger("applyBatchAuto");
|
||||
const ret = await runWithLock("procFiles", false, async () => {
|
||||
const procs = [...this.watchedFileEventQueue];
|
||||
this.watchedFileEventQueue = [];
|
||||
for (const queue of procs) {
|
||||
const file = queue.args.file;
|
||||
const cache = queue.args.cache;
|
||||
if ((queue.type == "CREATE" || queue.type == "CHANGED") && file instanceof TFile) {
|
||||
await this.updateIntoDB(file, false, cache);
|
||||
}
|
||||
if (queue.type == "DELETE") {
|
||||
if (file instanceof TFile) {
|
||||
await this.deleteFromDB(file);
|
||||
} else if (file instanceof TFolder) {
|
||||
await this.deleteFolderOnDB(file);
|
||||
}
|
||||
}
|
||||
if (queue.type == "RENAME") {
|
||||
await this.watchVaultRenameAsync(file, queue.args.oldPath);
|
||||
}
|
||||
}
|
||||
this.refreshStatusText();
|
||||
})
|
||||
this.refreshStatusText();
|
||||
return ret;
|
||||
}
|
||||
|
||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent("CREATE", file, null, ctx);
|
||||
}
|
||||
|
||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent("CHANGED", file, null, ctx);
|
||||
}
|
||||
|
||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent("DELETE", file, null, ctx);
|
||||
}
|
||||
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
||||
this.appendWatchEvent("RENAME", file, oldFile, ctx);
|
||||
}
|
||||
|
||||
watchWorkspaceOpen(file: TFile) {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (!this.isReady) return;
|
||||
this.watchWorkspaceOpenAsync(file);
|
||||
}
|
||||
|
||||
async watchWorkspaceOpenAsync(file: TFile) {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (!this.isReady) return;
|
||||
await this.applyBatchChange();
|
||||
if (file == null) {
|
||||
return;
|
||||
@@ -768,100 +897,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await this.showIfConflicted(file);
|
||||
}
|
||||
|
||||
watchVaultCreate(file: TFile, ...args: any[]) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (recentlyTouched(file)) {
|
||||
return;
|
||||
}
|
||||
this.watchVaultChangeAsync(file, ...args);
|
||||
}
|
||||
|
||||
watchVaultChange(file: TAbstractFile, ...args: any[]) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (recentlyTouched(file)) {
|
||||
return;
|
||||
}
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
|
||||
// If batchSave is enabled, queue all changes and do nothing.
|
||||
if (this.settings.batchSave) {
|
||||
~(async () => {
|
||||
const meta = await this.localDatabase.getDBEntryMeta(file.path);
|
||||
if (meta != false) {
|
||||
const localMtime = ~~(file.stat.mtime / 1000);
|
||||
const docMtime = ~~(meta.mtime / 1000);
|
||||
if (localMtime !== docMtime) {
|
||||
// Perhaps we have to modify (to using newer doc), but we don't be sure to every device's clock is adjusted.
|
||||
this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path]));
|
||||
this.refreshStatusText();
|
||||
}
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
this.watchVaultChangeAsync(file, ...args);
|
||||
}
|
||||
|
||||
async applyBatchChange() {
|
||||
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
|
||||
return;
|
||||
}
|
||||
return await runWithLock("batchSave", false, async () => {
|
||||
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
||||
this.batchFileChange = [];
|
||||
const semaphore = Semaphore(3);
|
||||
|
||||
const batchProcesses = batchItems.map(e => (async (e) => {
|
||||
const releaser = await semaphore.acquire(1, "batch");
|
||||
try {
|
||||
const f = this.app.vault.getAbstractFileByPath(normalizePath(e));
|
||||
if (f && f instanceof TFile) {
|
||||
await this.updateIntoDB(f);
|
||||
Logger(`Batch save:${e}`);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
} finally {
|
||||
releaser();
|
||||
}
|
||||
})(e))
|
||||
await Promise.all(batchProcesses);
|
||||
|
||||
this.refreshStatusText();
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
batchFileChange: string[] = [];
|
||||
|
||||
async watchVaultChangeAsync(file: TFile, ...args: any[]) {
|
||||
if (file instanceof TFile) {
|
||||
if (recentlyTouched(file)) {
|
||||
return;
|
||||
}
|
||||
await this.updateIntoDB(file);
|
||||
}
|
||||
}
|
||||
|
||||
watchVaultDelete(file: TAbstractFile) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
// When save is delayed, it should be cancelled.
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
this.watchVaultDeleteAsync(file).then(() => { });
|
||||
}
|
||||
|
||||
async watchVaultDeleteAsync(file: TAbstractFile) {
|
||||
if (file instanceof TFile) {
|
||||
await this.deleteFromDB(file);
|
||||
} else if (file instanceof TFolder) {
|
||||
await this.deleteFolderOnDB(file);
|
||||
}
|
||||
await this.procFileEvent(true);
|
||||
}
|
||||
|
||||
GetAllFilesRecursively(file: TAbstractFile): TFile[] {
|
||||
@@ -879,12 +916,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
watchVaultRename(file: TAbstractFile, oldFile: any) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
this.watchVaultRenameAsync(file, oldFile).then(() => { });
|
||||
}
|
||||
|
||||
getFilePath(file: TAbstractFile): string {
|
||||
if (file instanceof TFolder) {
|
||||
if (file.isRoot()) return "";
|
||||
@@ -897,7 +928,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return this.getFilePath(file.parent) + "/" + file.name;
|
||||
}
|
||||
|
||||
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any) {
|
||||
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any, cache?: CacheData) {
|
||||
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
try {
|
||||
await this.applyBatchChange();
|
||||
@@ -924,7 +955,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
} else if (file instanceof TFile) {
|
||||
try {
|
||||
Logger(`file save ${file.path} into db`);
|
||||
await this.updateIntoDB(file);
|
||||
await this.updateIntoDB(file, false, cache);
|
||||
Logger(`deleted ${oldFile} from db`);
|
||||
await this.deleteFromDBbyPath(oldFile);
|
||||
} catch (ex) {
|
||||
@@ -1046,7 +1077,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
Logger(msg + path);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
@@ -1066,7 +1097,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
Logger(msg + path);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
@@ -1134,7 +1165,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await this.ensureDirectory(path);
|
||||
try {
|
||||
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||
Logger(msg + path);
|
||||
const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile;
|
||||
touch(xf);
|
||||
@@ -1152,7 +1183,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
try {
|
||||
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||
Logger(msg + path);
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||
const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile;
|
||||
touch(xf);
|
||||
this.app.vault.trigger("modify", xf);
|
||||
@@ -1508,7 +1539,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.statusBar.title = this.localDatabase.syncStatus;
|
||||
let waiting = "";
|
||||
if (this.settings.batchSave) {
|
||||
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
|
||||
waiting = " " + this.watchedFileEventQueue.map((e) => "🛫").join("");
|
||||
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
||||
}
|
||||
let queued = "";
|
||||
@@ -1581,17 +1612,23 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async initializeDatabase(showingNotice?: boolean) {
|
||||
this.isReady = false;
|
||||
if (await this.openDatabase()) {
|
||||
if (this.localDatabase.isReady) {
|
||||
await this.syncAllFiles(showingNotice);
|
||||
}
|
||||
this.isReady = true;
|
||||
// run queued event once.
|
||||
await this.procFileEvent(true);
|
||||
return true;
|
||||
} else {
|
||||
this.isReady = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async replicateAllToServer(showingNotice?: boolean) {
|
||||
if (!this.isReady) return false;
|
||||
if (this.settings.autoSweepPlugins) {
|
||||
await this.sweepPlugin(showingNotice);
|
||||
}
|
||||
@@ -1754,23 +1791,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async renameFolder(folder: TFolder, oldFile: any) {
|
||||
for (const v of folder.children) {
|
||||
const entry = v as TFile & TFolder;
|
||||
if (entry.children) {
|
||||
await this.deleteFolderOnDB(entry);
|
||||
if (this.settings.trashInsteadDelete) {
|
||||
await this.app.vault.trash(entry, false);
|
||||
} else {
|
||||
await this.app.vault.delete(entry);
|
||||
}
|
||||
} else {
|
||||
await this.deleteFromDB(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --> conflict resolving
|
||||
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
|
||||
try {
|
||||
@@ -2027,20 +2047,30 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
}
|
||||
|
||||
async updateIntoDB(file: TFile, initialScan?: boolean) {
|
||||
async updateIntoDB(file: TFile, initialScan?: boolean, cache?: CacheData) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (shouldBeIgnored(file.path)) {
|
||||
return;
|
||||
}
|
||||
let content = "";
|
||||
let datatype: "plain" | "newnote" = "newnote";
|
||||
if (!isPlainText(file.name)) {
|
||||
const contentBin = await this.app.vault.readBinary(file);
|
||||
content = await arrayBufferToBase64(contentBin);
|
||||
datatype = "newnote";
|
||||
if (!cache) {
|
||||
if (!isPlainText(file.name)) {
|
||||
const contentBin = await this.app.vault.readBinary(file);
|
||||
content = await arrayBufferToBase64(contentBin);
|
||||
datatype = "newnote";
|
||||
} else {
|
||||
content = await this.app.vault.read(file);
|
||||
datatype = "plain";
|
||||
}
|
||||
} else {
|
||||
content = await this.app.vault.read(file);
|
||||
datatype = "plain";
|
||||
if (cache instanceof ArrayBuffer) {
|
||||
content = await arrayBufferToBase64(cache);
|
||||
datatype = "newnote"
|
||||
} else {
|
||||
content = cache;
|
||||
datatype = "plain";
|
||||
}
|
||||
}
|
||||
const fullPath = path2id(file.path);
|
||||
const d: LoadedEntry = {
|
||||
|
||||
Reference in New Issue
Block a user