mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2024-12-12 09:04:06 +02:00
Fixed:
- Now periodic internal file scanning works well. - The handler of Window-visibility-changed has been fixed. - And minor fixes possibly included. Refactored: - Unused logic has been removed. - Some utility functions have been moved into suitable files. - Function names have been renamed.
This commit is contained in:
parent
6b7956ab67
commit
9efb6ed0c1
@ -1,4 +1,4 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
import { App, Modal } from "./deps";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||
import { diff_result } from "./lib/src/types";
|
||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TFile, Modal, App } from "obsidian";
|
||||
import { TFile, Modal, App } from "./deps";
|
||||
import { isValidPath, path2id } from "./utils";
|
||||
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
import { App, Modal } from "./deps";
|
||||
import { LoadedEntry } from "./lib/src/types";
|
||||
import JsonResolvePane from "./JsonResolvePane.svelte";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "./deps";
|
||||
import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB.js";
|
||||
import { LocalPouchDBBase } from "./lib/src/LocalPouchDBBase.js";
|
||||
import { Logger } from "./lib/src/logger.js";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
import { App, Modal } from "./deps";
|
||||
import { logMessageStore } from "./lib/src/stores";
|
||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "obsidian";
|
||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps";
|
||||
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, ConfigPassphraseStore, RemoteDBSettings } from "./lib/src/types";
|
||||
import { path2id, id2path } from "./utils";
|
||||
import { delay } from "./lib/src/utils";
|
||||
@ -28,6 +28,7 @@ const requestToCouchDB = async (baseUri: string, username: string, password: str
|
||||
};
|
||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
selectedScreen = "";
|
||||
|
||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(app, plugin);
|
||||
@ -93,6 +94,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
element.addClass("selected");
|
||||
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = true;
|
||||
});
|
||||
this.selectedScreen = screen;
|
||||
};
|
||||
menuTabs.forEach((element) => {
|
||||
const e = element.querySelector(".sls-setting-tab");
|
||||
@ -1392,29 +1394,51 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
Logger("Select any preset.", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
this.plugin.settings.batchSave = false;
|
||||
this.plugin.settings.liveSync = false;
|
||||
this.plugin.settings.periodicReplication = false;
|
||||
this.plugin.settings.syncOnSave = false;
|
||||
this.plugin.settings.syncOnStart = false;
|
||||
this.plugin.settings.syncOnFileOpen = false;
|
||||
this.plugin.settings.syncAfterMerge = false;
|
||||
const presetAllDisabled = {
|
||||
batchSave: false,
|
||||
liveSync: false,
|
||||
periodicReplication: false,
|
||||
syncOnSave: false,
|
||||
syncOnStart: false,
|
||||
syncOnFileOpen: false,
|
||||
syncAfterMerge: false,
|
||||
} as Partial<ObsidianLiveSyncSettings>;
|
||||
const presetLiveSync = {
|
||||
...presetAllDisabled,
|
||||
liveSync: true
|
||||
} as Partial<ObsidianLiveSyncSettings>;
|
||||
const presetPeriodic = {
|
||||
...presetAllDisabled,
|
||||
batchSave: true,
|
||||
periodicReplication: true,
|
||||
syncOnSave: false,
|
||||
syncOnStart: true,
|
||||
syncOnFileOpen: true,
|
||||
syncAfterMerge: true,
|
||||
} as Partial<ObsidianLiveSyncSettings>;
|
||||
|
||||
if (currentPreset == "LIVESYNC") {
|
||||
this.plugin.settings.liveSync = true;
|
||||
this.plugin.settings = {
|
||||
...this.plugin.settings,
|
||||
...presetLiveSync
|
||||
}
|
||||
Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL.NOTICE);
|
||||
} else if (currentPreset == "PERIODIC") {
|
||||
this.plugin.settings.batchSave = true;
|
||||
this.plugin.settings.periodicReplication = true;
|
||||
this.plugin.settings.syncOnSave = false;
|
||||
this.plugin.settings.syncOnStart = true;
|
||||
this.plugin.settings.syncOnFileOpen = true;
|
||||
this.plugin.settings.syncAfterMerge = true;
|
||||
this.plugin.settings = {
|
||||
...this.plugin.settings,
|
||||
...presetPeriodic
|
||||
}
|
||||
Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL.NOTICE);
|
||||
} else {
|
||||
Logger("All synchronization disabled.", LOG_LEVEL.NOTICE);
|
||||
this.plugin.settings = {
|
||||
...this.plugin.settings,
|
||||
...presetAllDisabled
|
||||
}
|
||||
}
|
||||
this.plugin.saveSettings();
|
||||
await this.plugin.realizeSettingSyncMode();
|
||||
this.display();
|
||||
if (inWizard) {
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close()
|
||||
@ -1432,8 +1456,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// @ts-ignore
|
||||
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri")
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
);
|
||||
|
||||
@ -1769,18 +1791,22 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
}
|
||||
applyDisplayEnabled();
|
||||
addScreenElement("70", containerCorruptedDataEl);
|
||||
if (lastVersion != this.plugin.settings.lastReadUpdates) {
|
||||
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) {
|
||||
changeDisplay("100");
|
||||
if (this.selectedScreen == "") {
|
||||
if (lastVersion != this.plugin.settings.lastReadUpdates) {
|
||||
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) {
|
||||
changeDisplay("100");
|
||||
} else {
|
||||
changeDisplay("110")
|
||||
}
|
||||
} else {
|
||||
changeDisplay("110")
|
||||
if (isAnySyncEnabled()) {
|
||||
changeDisplay("0");
|
||||
} else {
|
||||
changeDisplay("110")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isAnySyncEnabled()) {
|
||||
changeDisplay("0");
|
||||
} else {
|
||||
changeDisplay("110")
|
||||
}
|
||||
changeDisplay(this.selectedScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
172
src/StorageEventManager.ts
Normal file
172
src/StorageEventManager.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { Plugin_2, TAbstractFile, TFile, TFolder } from "./deps";
|
||||
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
||||
import { getGlobalStore } from "./lib/src/store";
|
||||
import { ObsidianLiveSyncSettings } from "./lib/src/types";
|
||||
import { FileEventItem, FileEventType, FileInfo, InternalFileInfo, queueItem } from "./types";
|
||||
import { recentlyTouched } from "./utils";
|
||||
|
||||
|
||||
export abstract class StorageEventManager {
|
||||
abstract fetchEvent(): FileEventItem | false;
|
||||
abstract cancelRelativeEvent(item: FileEventItem): void;
|
||||
abstract getQueueLength(): number;
|
||||
}
|
||||
|
||||
type LiveSyncForStorageEventManager = Plugin_2 &
|
||||
{
|
||||
settings: ObsidianLiveSyncSettings
|
||||
} & {
|
||||
isTargetFile: (file: string | TAbstractFile) => boolean,
|
||||
procFileEvent: (applyBatch?: boolean) => Promise<boolean>
|
||||
};
|
||||
|
||||
|
||||
export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
plugin: LiveSyncForStorageEventManager;
|
||||
queuedFilesStore = getGlobalStore("queuedFiles", { queuedItems: [] as queueItem[], fileEventItems: [] as FileEventItem[] });
|
||||
|
||||
watchedFileEventQueue = [] as FileEventItem[];
|
||||
|
||||
constructor(plugin: LiveSyncForStorageEventManager) {
|
||||
super();
|
||||
this.plugin = plugin;
|
||||
this.watchVaultChange = this.watchVaultChange.bind(this);
|
||||
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||
this.watchVaultRename = this.watchVaultRename.bind(this);
|
||||
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
|
||||
plugin.registerEvent(app.vault.on("modify", this.watchVaultChange));
|
||||
plugin.registerEvent(app.vault.on("delete", this.watchVaultDelete));
|
||||
plugin.registerEvent(app.vault.on("rename", this.watchVaultRename));
|
||||
plugin.registerEvent(app.vault.on("create", this.watchVaultCreate));
|
||||
//@ts-ignore : Internal API
|
||||
plugin.registerEvent(app.vault.on("raw", this.watchVaultRawEvents));
|
||||
}
|
||||
|
||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent([{ type: "CREATE", file }], ctx);
|
||||
}
|
||||
|
||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent([{ type: "CHANGED", file }], ctx);
|
||||
}
|
||||
|
||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent([{ type: "DELETE", file }], ctx);
|
||||
}
|
||||
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Watch raw events (Internal API)
|
||||
watchVaultRawEvents(path: string) {
|
||||
if (!this.plugin.settings.syncInternalFiles) return;
|
||||
if (!this.plugin.settings.watchInternalFileChanges) return;
|
||||
if (!path.startsWith(app.vault.configDir)) return;
|
||||
const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns
|
||||
.replace(/\n| /g, "")
|
||||
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
||||
if (ignorePatterns.some(e => path.match(e))) return;
|
||||
this.appendWatchEvent(
|
||||
[{
|
||||
type: "INTERNAL",
|
||||
file: { path, mtime: 0, ctime: 0, size: 0 }
|
||||
}], null);
|
||||
}
|
||||
|
||||
// Cache file and waiting to can be proceed.
|
||||
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
||||
let forcePerform = false;
|
||||
for (const param of params) {
|
||||
if (shouldBeIgnored(param.file.path)) {
|
||||
continue;
|
||||
}
|
||||
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.plugin.isTargetFile(file.path)) continue;
|
||||
if (this.plugin.settings.suspendFileWatching) continue;
|
||||
|
||||
let cache: null | string | ArrayBuffer;
|
||||
// new file or something changed, cache the changes.
|
||||
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
|
||||
if (recentlyTouched(file)) {
|
||||
continue;
|
||||
}
|
||||
if (!isPlainText(file.name)) {
|
||||
cache = await app.vault.readBinary(file);
|
||||
} else {
|
||||
// cache = await this.app.vault.read(file);
|
||||
cache = await app.vault.cachedRead(file);
|
||||
if (!cache) cache = await app.vault.read(file);
|
||||
}
|
||||
}
|
||||
if (type == "DELETE" || type == "RENAME") {
|
||||
forcePerform = true;
|
||||
}
|
||||
|
||||
|
||||
if (this.plugin.settings.batchSave) {
|
||||
// if the latest event is the same type, omit that
|
||||
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
|
||||
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
|
||||
// a.md MODIFY
|
||||
// a.md CREATE
|
||||
// :
|
||||
let i = this.watchedFileEventQueue.length;
|
||||
L1:
|
||||
while (i >= 0) {
|
||||
i--;
|
||||
if (i < 0) break L1;
|
||||
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
|
||||
continue L1;
|
||||
}
|
||||
if (this.watchedFileEventQueue[i].type != type) break L1;
|
||||
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
||||
//this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||
this.queuedFilesStore.apply((value) => ({ ...value, 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({
|
||||
type,
|
||||
args: {
|
||||
file: fileInfo,
|
||||
oldPath,
|
||||
cache,
|
||||
ctx
|
||||
},
|
||||
key: atomicKey
|
||||
})
|
||||
}
|
||||
// this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
||||
this.plugin.procFileEvent(forcePerform);
|
||||
}
|
||||
fetchEvent(): FileEventItem | false {
|
||||
if (this.watchedFileEventQueue.length == 0) return false;
|
||||
const item = this.watchedFileEventQueue.shift();
|
||||
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
||||
return item;
|
||||
}
|
||||
cancelRelativeEvent(item: FileEventItem) {
|
||||
this.watchedFileEventQueue = [...this.watchedFileEventQueue].filter(e => e.key != item.key);
|
||||
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
||||
}
|
||||
getQueueLength() {
|
||||
return this.watchedFileEventQueue.length;
|
||||
}
|
||||
}
|
4
src/deps.ts
Normal file
4
src/deps.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export {
|
||||
addIcon, App, DataWriteOptions, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, normalizePath, Notice, Platform, Plugin, PluginManifest,
|
||||
PluginSettingTab, Plugin_2, requestUrl, RequestUrlParam, RequestUrlResponse, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder
|
||||
} from "obsidian";
|
@ -1,4 +1,4 @@
|
||||
import { App, FuzzySuggestModal, Modal, Setting } from "obsidian";
|
||||
import { App, FuzzySuggestModal, Modal, Setting } from "./deps";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
|
||||
//@ts-ignore
|
||||
|
448
src/main.ts
448
src/main.ts
@ -1,7 +1,8 @@
|
||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App } from "obsidian";
|
||||
const isDebug = false;
|
||||
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "./deps";
|
||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry, SALT_OF_PASSPHRASE, ConfigPassphraseStore, CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE } from "./lib/src/types";
|
||||
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo, queueItem, FileInfo } from "./types";
|
||||
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo, queueItem, CacheData, FileEventItem, configURIBase, FileWatchEventQueueMax, PSCHeader, PSCHeaderEnd, ICHeader, ICHeaderEnd } from "./types";
|
||||
import { delay, getDocData, isDocContentSame } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { LocalPouchDB } from "./LocalPouchDB";
|
||||
@ -9,12 +10,9 @@ import { LogDisplayModal } from "./LogDisplayModal";
|
||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
import { applyPatch, clearAllPeriodic, clearAllTriggers, clearTrigger, disposeMemoObject, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, memoIfNotExist, memoObject, flattenObject, path2id, retrieveMemoObject, setTrigger, tryParseJSON, createFile, modifyFile, isValidPath } from "./utils";
|
||||
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, disposeMemoObject, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, memoIfNotExist, memoObject, flattenObject, path2id, retrieveMemoObject, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, trimPrefix, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, filename2idInternalMetadata, id2filenameInternalMetadata, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched } from "./utils";
|
||||
import { decrypt, encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
||||
|
||||
const isDebug = false;
|
||||
|
||||
import { InputStringDialog, PluginDialogModal, PopoverSelectString } from "./dialogs";
|
||||
import { PluginDialogModal } from "./dialogs";
|
||||
import { isCloudantURI, isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
||||
import { getGlobalStore, observeStores } from "./lib/src/store";
|
||||
import { lockStore, logMessageStore, logStore } from "./lib/src/stores";
|
||||
@ -24,110 +22,10 @@ import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
||||
import { runWithLock } from "./lib/src/lock";
|
||||
import { Semaphore } from "./lib/src/semaphore";
|
||||
import { JsonResolveModal } from "./JsonResolveModal";
|
||||
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
|
||||
|
||||
setNoticeClass(Notice);
|
||||
|
||||
const ICHeader = "i:";
|
||||
const ICHeaderEnd = "i;";
|
||||
const ICHeaderLength = ICHeader.length;
|
||||
const FileWatchEventQueueMax = 10;
|
||||
|
||||
const configURIBase = "obsidian://setuplivesync?settings=";
|
||||
|
||||
function getAbstractFileByPath(path: string): TAbstractFile | null {
|
||||
// Hidden API but so useful.
|
||||
// @ts-ignore
|
||||
if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
|
||||
// @ts-ignore
|
||||
return app.vault.getAbstractFileByPathInsensitive(path);
|
||||
} else {
|
||||
return app.vault.getAbstractFileByPath(path);
|
||||
}
|
||||
}
|
||||
function trimPrefix(target: string, prefix: string) {
|
||||
return target.startsWith(prefix) ? target.substring(prefix.length) : target;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns is internal chunk of file
|
||||
* @param str ID
|
||||
* @returns
|
||||
*/
|
||||
function isInternalMetadata(str: string): boolean {
|
||||
return str.startsWith(ICHeader);
|
||||
}
|
||||
function id2filenameInternalMetadata(str: string): string {
|
||||
return str.substring(ICHeaderLength);
|
||||
}
|
||||
function filename2idInternalMetadata(str: string): string {
|
||||
return ICHeader + str;
|
||||
}
|
||||
|
||||
const CHeader = "h:";
|
||||
// const CHeaderLength = CHeader.length;
|
||||
function isChunk(str: string): boolean {
|
||||
return str.startsWith(CHeader);
|
||||
}
|
||||
|
||||
const PSCHeader = "ps:";
|
||||
const PSCHeaderEnd = "ps;";
|
||||
function isPluginMetadata(str: string): boolean {
|
||||
return str.startsWith(PSCHeader);
|
||||
}
|
||||
|
||||
|
||||
const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
||||
return new Promise((res) => {
|
||||
const popover = new PopoverSelectString(app, message, null, null, (result) => res(result as "yes" | "no"));
|
||||
popover.open();
|
||||
});
|
||||
};
|
||||
|
||||
const askSelectString = (app: App, message: string, items: string[]): Promise<string> => {
|
||||
const getItemsFun = () => items;
|
||||
return new Promise((res) => {
|
||||
const popover = new PopoverSelectString(app, message, "", getItemsFun, (result) => res(result));
|
||||
popover.open();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => {
|
||||
return new Promise((res) => {
|
||||
const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result));
|
||||
dialog.open();
|
||||
});
|
||||
};
|
||||
let touchedFiles: string[] = [];
|
||||
function touch(file: TFile | string) {
|
||||
const f = file instanceof TFile ? file : getAbstractFileByPath(file) as TFile;
|
||||
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
|
||||
touchedFiles.unshift(key);
|
||||
touchedFiles = touchedFiles.slice(0, 100);
|
||||
}
|
||||
function recentlyTouched(file: TFile) {
|
||||
const key = `${file.path}-${file.stat.mtime}-${file.stat.size}`;
|
||||
if (touchedFiles.indexOf(key) == -1) return false;
|
||||
return true;
|
||||
}
|
||||
function clearTouched() {
|
||||
touchedFiles = [];
|
||||
}
|
||||
|
||||
type CacheData = string | ArrayBuffer;
|
||||
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
||||
type FileEventArgs = {
|
||||
file: FileInfo | InternalFileInfo;
|
||||
cache?: CacheData;
|
||||
oldPath?: string;
|
||||
ctx?: any;
|
||||
}
|
||||
type FileEventItem = {
|
||||
type: FileEventType,
|
||||
args: FileEventArgs,
|
||||
key: string,
|
||||
}
|
||||
|
||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
settings: ObsidianLiveSyncSettings;
|
||||
localDatabase: LocalPouchDB;
|
||||
@ -140,7 +38,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
packageVersion = "";
|
||||
manifestVersion = "";
|
||||
|
||||
watchedFileEventQueue = [] as FileEventItem[];
|
||||
periodicSyncProcessor = new PeriodicProcessor(this, async () => await this.replicate());
|
||||
periodicPluginSweepProcessor = new PeriodicProcessor(this, async () => await this.sweepPlugin(false));
|
||||
periodicInternalFileScanProcessor = new PeriodicProcessor(this, async () => await this.syncInternalFilesAndDatabase("push", false));
|
||||
|
||||
getVaultName(): string {
|
||||
return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
|
||||
@ -544,21 +444,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
localStorage.setItem(lsKey, `${VER}`);
|
||||
await this.openDatabase();
|
||||
|
||||
this.watchVaultChange = this.watchVaultChange.bind(this);
|
||||
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||
this.watchVaultRename = this.watchVaultRename.bind(this);
|
||||
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
|
||||
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false);
|
||||
this.watchWindowVisibility = debounce(this.watchWindowVisibility.bind(this), 1000, false);
|
||||
this.watchOnline = debounce(this.watchOnline.bind(this), 500, false);
|
||||
|
||||
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
||||
|
||||
this.setPeriodicSync = this.setPeriodicSync.bind(this);
|
||||
this.clearPeriodicSync = this.clearPeriodicSync.bind(this);
|
||||
this.periodicSync = this.periodicSync.bind(this);
|
||||
this.loadQueuedFiles = this.loadQueuedFiles.bind(this);
|
||||
|
||||
this.getPluginList = this.getPluginList.bind(this);
|
||||
@ -764,21 +655,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (this.localDatabase != null) {
|
||||
this.localDatabase.onunload();
|
||||
}
|
||||
if (this.gcTimerHandler != null) {
|
||||
clearTimeout(this.gcTimerHandler);
|
||||
this.gcTimerHandler = null;
|
||||
}
|
||||
this.clearPeriodicSync();
|
||||
this.clearPluginSweep();
|
||||
this.clearInternalFileScan();
|
||||
|
||||
this.periodicSyncProcessor?.disable();
|
||||
this.periodicPluginSweepProcessor?.disable();
|
||||
this.periodicInternalFileScanProcessor?.disable();
|
||||
if (this.localDatabase != null) {
|
||||
this.localDatabase.closeReplication();
|
||||
this.localDatabase.close();
|
||||
}
|
||||
clearAllPeriodic();
|
||||
clearAllTriggers();
|
||||
window.removeEventListener("visibilitychange", this.watchWindowVisibility);
|
||||
window.removeEventListener("online", this.watchOnline);
|
||||
cancelAllPeriodicTask();
|
||||
cancelAllTasks();
|
||||
Logger("unloading plugin");
|
||||
}
|
||||
|
||||
@ -940,21 +826,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.triggerRealizeSettingSyncMode();
|
||||
}
|
||||
|
||||
gcTimerHandler: any = null;
|
||||
|
||||
vaultManager: StorageEventManager;
|
||||
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));
|
||||
//@ts-ignore : Internal API
|
||||
this.registerEvent(this.app.vault.on("raw", this.watchVaultRawEvents));
|
||||
this.vaultManager = new StorageEventManagerObsidian(this)
|
||||
}
|
||||
|
||||
registerWatchEvents() {
|
||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||
window.addEventListener("visibilitychange", this.watchWindowVisibility);
|
||||
window.addEventListener("online", this.watchOnline);
|
||||
this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
|
||||
this.registerDomEvent(window, "online", this.watchOnline);
|
||||
this.registerDomEvent(window, "offline", this.watchOnline);
|
||||
}
|
||||
|
||||
|
||||
@ -963,6 +844,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
async watchOnlineAsync() {
|
||||
// If some files were failed to retrieve, scan files again.
|
||||
// TODO:FIXME AT V0.17.31, this logic has been disabled.
|
||||
if (navigator.onLine && this.localDatabase.needScanning) {
|
||||
this.localDatabase.needScanning = false;
|
||||
await this.syncAllFiles();
|
||||
@ -980,7 +862,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await this.applyBatchChange();
|
||||
if (isHidden) {
|
||||
this.localDatabase.closeReplication();
|
||||
this.clearPeriodicSync();
|
||||
this.periodicSyncProcessor?.disable();
|
||||
} else {
|
||||
// suspend all temporary.
|
||||
if (this.suspended) return;
|
||||
@ -993,175 +875,68 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (this.settings.syncOnStart) {
|
||||
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
||||
}
|
||||
if (this.settings.periodicReplication) {
|
||||
this.setPeriodicSync();
|
||||
}
|
||||
this.periodicSyncProcessor.enable(this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache file and waiting to can be proceed.
|
||||
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
||||
let forcePerform = false;
|
||||
for (const param of params) {
|
||||
if (shouldBeIgnored(param.file.path)) {
|
||||
continue;
|
||||
}
|
||||
const atomicKey = [0, 0, 0, 0, 0, 0].map(e => `${Math.floor(Math.random() * 100000)}`).join("-");
|
||||
const type = param.type;
|
||||
const file = param.file;
|
||||
const oldPath = param.oldPath;
|
||||
if (file instanceof TFolder) continue;
|
||||
if (!this.isTargetFile(file.path)) continue;
|
||||
if (this.settings.suspendFileWatching) continue;
|
||||
|
||||
let cache: null | string | ArrayBuffer;
|
||||
// new file or something changed, cache the changes.
|
||||
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
|
||||
if (recentlyTouched(file)) {
|
||||
continue;
|
||||
}
|
||||
if (!isPlainText(file.name)) {
|
||||
cache = await this.app.vault.readBinary(file);
|
||||
} else {
|
||||
// cache = await this.app.vault.read(file);
|
||||
cache = await this.app.vault.cachedRead(file);
|
||||
if (!cache) cache = await this.app.vault.read(file);
|
||||
}
|
||||
}
|
||||
if (type == "DELETE" || type == "RENAME") {
|
||||
forcePerform = true;
|
||||
}
|
||||
|
||||
|
||||
if (this.settings.batchSave) {
|
||||
// if the latest event is the same type, omit that
|
||||
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
|
||||
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
|
||||
// a.md MODIFY
|
||||
// a.md CREATE
|
||||
// :
|
||||
let i = this.watchedFileEventQueue.length;
|
||||
L1:
|
||||
while (i >= 0) {
|
||||
i--;
|
||||
if (i < 0) break L1;
|
||||
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
|
||||
continue L1;
|
||||
}
|
||||
if (this.watchedFileEventQueue[i].type != type) break L1;
|
||||
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||
}
|
||||
}
|
||||
|
||||
const fileInfo = file instanceof TFile ? {
|
||||
ctime: file.stat.ctime,
|
||||
mtime: file.stat.mtime,
|
||||
file: file,
|
||||
path: file.path,
|
||||
size: file.stat.size
|
||||
} as FileInfo : file as InternalFileInfo;
|
||||
this.watchedFileEventQueue.push({
|
||||
type,
|
||||
args: {
|
||||
file: fileInfo,
|
||||
oldPath,
|
||||
cache,
|
||||
ctx
|
||||
},
|
||||
key: atomicKey
|
||||
})
|
||||
}
|
||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||
if (this.isReady) {
|
||||
await this.procFileEvent(forcePerform);
|
||||
}
|
||||
|
||||
}
|
||||
async procFileEvent(applyBatch?: boolean) {
|
||||
if (!this.isReady) return;
|
||||
if (this.settings.batchSave) {
|
||||
if (!applyBatch && this.watchedFileEventQueue.length < FileWatchEventQueueMax) {
|
||||
if (!applyBatch && this.vaultManager.getQueueLength() < FileWatchEventQueueMax) {
|
||||
// Defer till applying batch save or queue has been grown enough.
|
||||
// or 120 seconds after.
|
||||
setTrigger("applyBatchAuto", 30000, () => {
|
||||
// or 30 seconds after.
|
||||
scheduleTask("applyBatchAuto", 30000, () => {
|
||||
this.procFileEvent(true);
|
||||
})
|
||||
return;
|
||||
}
|
||||
}
|
||||
clearTrigger("applyBatchAuto");
|
||||
cancelTask("applyBatchAuto");
|
||||
const ret = await runWithLock("procFiles", true, async () => {
|
||||
L2:
|
||||
do {
|
||||
const procs = [...this.watchedFileEventQueue];
|
||||
this.watchedFileEventQueue = [];
|
||||
const queue = this.vaultManager.fetchEvent();
|
||||
if (queue === false) break;
|
||||
if (queue === undefined) break;
|
||||
const file = queue.args.file;
|
||||
const key = `file-last-proc-${queue.type}-${file.path}`;
|
||||
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
|
||||
if (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;
|
||||
}
|
||||
//TODO: check from cache time.
|
||||
if (file.mtime == last) {
|
||||
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
continue;
|
||||
}
|
||||
|
||||
L1:
|
||||
do {
|
||||
const queue = procs.shift();
|
||||
if (queue === undefined) break L1;
|
||||
|
||||
const file = queue.args.file;
|
||||
const key = `file-last-proc-${queue.type}-${file.path}`;
|
||||
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
|
||||
if (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);
|
||||
continue L1;
|
||||
}
|
||||
|
||||
const cache = queue.args.cache;
|
||||
if (queue.type == "CREATE" || queue.type == "CHANGED") {
|
||||
if (!await this.updateIntoDB(targetFile, false, cache)) {
|
||||
Logger(`DB -> STORAGE: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL.INFO);
|
||||
// cancel running queues and remove one of atomic operation
|
||||
this.watchedFileEventQueue = [...procs, ...this.watchedFileEventQueue].filter(e => e.key != queue.key);
|
||||
continue L2;
|
||||
}
|
||||
}
|
||||
if (queue.type == "RENAME") {
|
||||
// Obsolete
|
||||
await this.watchVaultRenameAsync(targetFile, queue.args.oldPath);
|
||||
const cache = queue.args.cache;
|
||||
if (queue.type == "CREATE" || queue.type == "CHANGED") {
|
||||
if (!await this.updateIntoDB(targetFile, false, cache)) {
|
||||
Logger(`DB -> STORAGE: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL.INFO);
|
||||
// cancel running queues and remove one of atomic operation
|
||||
this.vaultManager.cancelRelativeEvent(queue);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await this.localDatabase.kvDB.set(key, file.mtime);
|
||||
} while (procs.length > 0);
|
||||
} while (this.watchedFileEventQueue.length != 0);
|
||||
if (queue.type == "RENAME") {
|
||||
// Obsolete
|
||||
await this.watchVaultRenameAsync(targetFile, queue.args.oldPath);
|
||||
}
|
||||
}
|
||||
await this.localDatabase.kvDB.set(key, file.mtime);
|
||||
} while (this.vaultManager.getQueueLength() > 0);
|
||||
return true;
|
||||
})
|
||||
return ret;
|
||||
}
|
||||
|
||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent([{ type: "CREATE", file }], ctx);
|
||||
}
|
||||
|
||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent([{ type: "CHANGED", file }], ctx);
|
||||
}
|
||||
|
||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||
this.appendWatchEvent([{ type: "DELETE", file }], ctx);
|
||||
}
|
||||
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
||||
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) {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
@ -1188,21 +963,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// Watch raw events (Internal API)
|
||||
watchVaultRawEvents(path: string) {
|
||||
if (!this.settings.syncInternalFiles) return;
|
||||
if (!this.settings.watchInternalFileChanges) return;
|
||||
if (!path.startsWith(this.app.vault.configDir)) return;
|
||||
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
|
||||
.replace(/\n| /g, "")
|
||||
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
||||
if (ignorePatterns.some(e => path.match(e))) return;
|
||||
this.appendWatchEvent(
|
||||
[{
|
||||
type: "INTERNAL",
|
||||
file: { path, mtime: 0, ctime: 0, size: 0 }
|
||||
}], null);
|
||||
}
|
||||
recentProcessedInternalFiles = [] as string[];
|
||||
async watchVaultRawEventsAsync(path: string) {
|
||||
const stat = await this.app.vault.adapter.stat(path);
|
||||
@ -1571,7 +1331,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
procInternalFile(filename: string) {
|
||||
this.procInternalFiles.push(filename);
|
||||
setTrigger("procInternal", 500, async () => {
|
||||
scheduleTask("procInternal", 500, async () => {
|
||||
await this.execInternalFile();
|
||||
});
|
||||
}
|
||||
@ -1600,7 +1360,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
this.queuedFiles = this.queuedFiles.filter((e) => !e.done);
|
||||
this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
||||
this.queuedFilesStore.apply((value) => ({ ...value, queuedItems: this.queuedFiles }));
|
||||
this.saveQueuedFiles();
|
||||
}
|
||||
parseIncomingChunk(chunk: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||
@ -1673,7 +1433,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.saveQueuedFiles();
|
||||
this.procQueuedFiles();
|
||||
}
|
||||
periodicSyncHandler: number = null;
|
||||
|
||||
//---> Sync
|
||||
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
|
||||
@ -1739,50 +1498,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
}
|
||||
|
||||
clearPeriodicSync() {
|
||||
if (this.periodicSyncHandler != null) {
|
||||
clearInterval(this.periodicSyncHandler);
|
||||
this.periodicSyncHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
setPeriodicSync() {
|
||||
this.clearPeriodicSync();
|
||||
if (this.settings.periodicReplication && this.settings.periodicReplicationInterval > 0) {
|
||||
this.periodicSyncHandler = this.setInterval(async () => await this.periodicSync(), Math.max(this.settings.periodicReplicationInterval, 30) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async periodicSync() {
|
||||
await this.replicate();
|
||||
}
|
||||
|
||||
periodicPluginSweepHandler: number = null;
|
||||
|
||||
clearPluginSweep() {
|
||||
if (this.periodicPluginSweepHandler != null) {
|
||||
clearInterval(this.periodicPluginSweepHandler);
|
||||
this.periodicPluginSweepHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
setPluginSweep() {
|
||||
if (this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges) {
|
||||
this.clearPluginSweep();
|
||||
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicPluginSweep(), PERIODIC_PLUGIN_SWEEP * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async periodicPluginSweep() {
|
||||
await this.sweepPlugin(false);
|
||||
}
|
||||
|
||||
async realizeSettingSyncMode() {
|
||||
this.localDatabase.closeReplication();
|
||||
this.clearPeriodicSync();
|
||||
this.clearPluginSweep();
|
||||
this.clearInternalFileScan();
|
||||
this.periodicSyncProcessor?.disable();
|
||||
this.periodicPluginSweepProcessor?.disable();
|
||||
this.periodicInternalFileScanProcessor?.disable();
|
||||
await this.applyBatchChange();
|
||||
// disable all sync temporary.
|
||||
if (this.suspended) return;
|
||||
@ -1795,9 +1519,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (this.settings.syncInternalFiles) {
|
||||
await this.syncInternalFilesAndDatabase("safe", false);
|
||||
}
|
||||
this.setPeriodicSync();
|
||||
this.setPluginSweep();
|
||||
this.setPeriodicInternalFileScan();
|
||||
this.periodicSyncProcessor.enable(this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0);
|
||||
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0)
|
||||
this.periodicInternalFileScanProcessor.enable(this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0)
|
||||
}
|
||||
|
||||
lastMessage = "";
|
||||
@ -1842,7 +1566,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.statusBar.title = e.syncStatus;
|
||||
let waiting = "";
|
||||
if (this.settings.batchSave) {
|
||||
waiting = " " + this.watchedFileEventQueue.map((e) => "🛫").join("");
|
||||
waiting = " " + "🛫".repeat(this.vaultManager.getQueueLength());
|
||||
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
||||
}
|
||||
let queued = "";
|
||||
@ -3011,27 +2735,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
|
||||
periodicInternalFileScanHandler: number = null;
|
||||
|
||||
clearInternalFileScan() {
|
||||
if (this.periodicInternalFileScanHandler != null) {
|
||||
clearInterval(this.periodicInternalFileScanHandler);
|
||||
this.periodicInternalFileScanHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
setPeriodicInternalFileScan() {
|
||||
if (this.periodicInternalFileScanHandler != null) {
|
||||
this.clearInternalFileScan();
|
||||
}
|
||||
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval > 0 && !this.settings.watchInternalFileChanges) {
|
||||
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicInternalFileScan(), this.settings.syncInternalFilesInterval * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async periodicInternalFileScan() {
|
||||
await this.syncInternalFilesAndDatabase("push", false);
|
||||
}
|
||||
|
||||
async getFiles(
|
||||
path: string,
|
||||
@ -3539,14 +3242,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
|
||||
const updatedPluginKey = "popupUpdated-" + updatePluginId;
|
||||
setTrigger(updatedPluginKey, 1000, async () => {
|
||||
scheduleTask(updatedPluginKey, 1000, async () => {
|
||||
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
||||
//@ts-ignore
|
||||
const isShown = popup?.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
memoObject(updatedPluginKey, new Notice(fragment, 0))
|
||||
}
|
||||
setTrigger(updatedPluginKey + "-close", 20000, () => {
|
||||
scheduleTask(updatedPluginKey + "-close", 20000, () => {
|
||||
const popup = retrieveMemoObject<Notice>(updatedPluginKey)
|
||||
if (!popup) return;
|
||||
//@ts-ignore
|
||||
@ -3581,13 +3284,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
});
|
||||
|
||||
setTrigger("popupUpdated-" + configDir, 1000, () => {
|
||||
scheduleTask("popupUpdated-" + configDir, 1000, () => {
|
||||
//@ts-ignore
|
||||
const isShown = this.confirmPopup?.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
this.confirmPopup = new Notice(fragment, 0);
|
||||
}
|
||||
setTrigger("popupClose" + configDir, 20000, () => {
|
||||
scheduleTask("popupClose" + configDir, 20000, () => {
|
||||
this.confirmPopup?.hide();
|
||||
this.confirmPopup = null;
|
||||
})
|
||||
@ -3608,3 +3311,4 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
28
src/types.ts
28
src/types.ts
@ -1,4 +1,4 @@
|
||||
import { PluginManifest, TFile } from "obsidian";
|
||||
import { PluginManifest, TFile } from "./deps";
|
||||
import { DatabaseEntry, EntryBody } from "./lib/src/types";
|
||||
|
||||
export interface PluginDataEntry extends DatabaseEntry {
|
||||
@ -46,4 +46,28 @@ export type queueItem = {
|
||||
timeout?: number;
|
||||
done?: boolean;
|
||||
warned?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type CacheData = string | ArrayBuffer;
|
||||
export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
||||
export type FileEventArgs = {
|
||||
file: FileInfo | InternalFileInfo;
|
||||
cache?: CacheData;
|
||||
oldPath?: string;
|
||||
ctx?: any;
|
||||
}
|
||||
export type FileEventItem = {
|
||||
type: FileEventType,
|
||||
args: FileEventArgs,
|
||||
key: string,
|
||||
}
|
||||
|
||||
export const CHeader = "h:";
|
||||
export const PSCHeader = "ps:";
|
||||
export const PSCHeaderEnd = "ps;";
|
||||
export const ICHeader = "i:";
|
||||
export const ICHeaderEnd = "i;";
|
||||
export const ICHeaderLength = ICHeader.length;
|
||||
|
||||
export const FileWatchEventQueueMax = 10;
|
||||
export const configURIBase = "obsidian://setuplivesync?settings=";
|
145
src/utils.ts
145
src/utils.ts
@ -1,8 +1,10 @@
|
||||
import { DataWriteOptions, normalizePath, TFile, Platform } from "obsidian";
|
||||
import { DataWriteOptions, normalizePath, TFile, Platform, TAbstractFile, App, Plugin_2 } from "./deps";
|
||||
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid } from "./lib/src/path";
|
||||
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { LOG_LEVEL } from "./lib/src/types";
|
||||
import { CHeader, ICHeader, ICHeaderLength, PSCHeader } from "./types";
|
||||
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
||||
|
||||
// For backward compatibility, using the path for determining id.
|
||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
@ -15,40 +17,44 @@ export function id2path(filename: string): string {
|
||||
return id2path_base(normalizePath(filename));
|
||||
}
|
||||
|
||||
const triggers: { [key: string]: ReturnType<typeof setTimeout> } = {};
|
||||
export function setTrigger(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
||||
clearTrigger(key);
|
||||
triggers[key] = setTimeout(async () => {
|
||||
delete triggers[key];
|
||||
const tasks: { [key: string]: ReturnType<typeof setTimeout> } = {};
|
||||
export function scheduleTask(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
||||
cancelTask(key);
|
||||
tasks[key] = setTimeout(async () => {
|
||||
delete tasks[key];
|
||||
await proc();
|
||||
}, timeout);
|
||||
}
|
||||
export function clearTrigger(key: string) {
|
||||
if (key in triggers) {
|
||||
clearTimeout(triggers[key]);
|
||||
export function cancelTask(key: string) {
|
||||
if (key in tasks) {
|
||||
clearTimeout(tasks[key]);
|
||||
delete tasks[key];
|
||||
}
|
||||
}
|
||||
export function clearAllTriggers() {
|
||||
for (const v in triggers) {
|
||||
clearTimeout(triggers[v]);
|
||||
export function cancelAllTasks() {
|
||||
for (const v in tasks) {
|
||||
clearTimeout(tasks[v]);
|
||||
delete tasks[v];
|
||||
}
|
||||
}
|
||||
const intervals: { [key: string]: ReturnType<typeof setInterval> } = {};
|
||||
export function setPeriodic(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
||||
clearPeriodic(key);
|
||||
export function setPeriodicTask(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
||||
cancelPeriodicTask(key);
|
||||
intervals[key] = setInterval(async () => {
|
||||
delete intervals[key];
|
||||
await proc();
|
||||
}, timeout);
|
||||
}
|
||||
export function clearPeriodic(key: string) {
|
||||
export function cancelPeriodicTask(key: string) {
|
||||
if (key in intervals) {
|
||||
clearInterval(intervals[key]);
|
||||
delete intervals[key];
|
||||
}
|
||||
}
|
||||
export function clearAllPeriodic() {
|
||||
export function cancelAllPeriodicTask() {
|
||||
for (const v in intervals) {
|
||||
clearInterval(intervals[v]);
|
||||
delete intervals[v];
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,4 +296,109 @@ export function isValidPath(filename: string) {
|
||||
//Fallback
|
||||
Logger("Could not determine platform for checking filename", LOG_LEVEL.VERBOSE);
|
||||
return isValidFilenameInWidows(filename);
|
||||
}
|
||||
}
|
||||
|
||||
let touchedFiles: string[] = [];
|
||||
|
||||
export function getAbstractFileByPath(path: string): TAbstractFile | null {
|
||||
// Hidden API but so useful.
|
||||
// @ts-ignore
|
||||
if ("getAbstractFileByPathInsensitive" in app.vault && (app.vault.adapter?.insensitive ?? false)) {
|
||||
// @ts-ignore
|
||||
return app.vault.getAbstractFileByPathInsensitive(path);
|
||||
} else {
|
||||
return app.vault.getAbstractFileByPath(path);
|
||||
}
|
||||
}
|
||||
export function trimPrefix(target: string, prefix: string) {
|
||||
return target.startsWith(prefix) ? target.substring(prefix.length) : target;
|
||||
}
|
||||
|
||||
export function touch(file: TFile | string) {
|
||||
const f = file instanceof TFile ? file : getAbstractFileByPath(file) as TFile;
|
||||
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
|
||||
touchedFiles.unshift(key);
|
||||
touchedFiles = touchedFiles.slice(0, 100);
|
||||
}
|
||||
export function recentlyTouched(file: TFile) {
|
||||
const key = `${file.path}-${file.stat.mtime}-${file.stat.size}`;
|
||||
if (touchedFiles.indexOf(key) == -1) return false;
|
||||
return true;
|
||||
}
|
||||
export function clearTouched() {
|
||||
touchedFiles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* returns is internal chunk of file
|
||||
* @param str ID
|
||||
* @returns
|
||||
*/
|
||||
export function isInternalMetadata(str: string): boolean {
|
||||
return str.startsWith(ICHeader);
|
||||
}
|
||||
export function id2filenameInternalMetadata(str: string): string {
|
||||
return str.substring(ICHeaderLength);
|
||||
}
|
||||
export function filename2idInternalMetadata(str: string): string {
|
||||
return ICHeader + str;
|
||||
}
|
||||
|
||||
// const CHeaderLength = CHeader.length;
|
||||
export function isChunk(str: string): boolean {
|
||||
return str.startsWith(CHeader);
|
||||
}
|
||||
|
||||
export function isPluginMetadata(str: string): boolean {
|
||||
return str.startsWith(PSCHeader);
|
||||
}
|
||||
|
||||
export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
||||
return new Promise((res) => {
|
||||
const popover = new PopoverSelectString(app, message, null, null, (result) => res(result as "yes" | "no"));
|
||||
popover.open();
|
||||
});
|
||||
};
|
||||
|
||||
export const askSelectString = (app: App, message: string, items: string[]): Promise<string> => {
|
||||
const getItemsFun = () => items;
|
||||
return new Promise((res) => {
|
||||
const popover = new PopoverSelectString(app, message, "", getItemsFun, (result) => res(result));
|
||||
popover.open();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => {
|
||||
return new Promise((res) => {
|
||||
const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result));
|
||||
dialog.open();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export class PeriodicProcessor {
|
||||
_process: () => Promise<any>;
|
||||
_timer?: number;
|
||||
_plugin: Plugin_2;
|
||||
constructor(plugin: Plugin_2, process: () => Promise<any>) {
|
||||
this._plugin = plugin;
|
||||
this._process = process;
|
||||
}
|
||||
async process() {
|
||||
try {
|
||||
await this._process();
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
enable(interval: number) {
|
||||
this.disable();
|
||||
if (interval == 0) return;
|
||||
this._timer = window.setInterval(() => this._process().then(() => { }), interval);
|
||||
this._plugin.registerInterval(this._timer);
|
||||
}
|
||||
disable() {
|
||||
if (this._timer) clearInterval(this._timer);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user