mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-23 22:51:30 +00:00
- Improved:
- Now we can preserve the logs into the file.
- Note: This option will be enabled automatically also when we flagging a red flag.
- File names can now be made platform-appropriate.
- Refactored:
- Some redundant implementations have been sorted out.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { TFile, Modal, App } from "obsidian";
|
||||
import { path2id } from "./utils";
|
||||
import { isValidPath, path2id } from "./utils";
|
||||
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
|
||||
import { isValidPath } from "./lib/src/path";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||
import { LoadedEntry, LOG_LEVEL } from "./lib/src/types";
|
||||
|
||||
@@ -1538,6 +1538,15 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Write logs into the file")
|
||||
.setDesc("Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.writeLogToTheFile).onChange(async (value) => {
|
||||
this.plugin.settings.writeLogToTheFile = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Discard local database to reset or uninstall Self-hosted LiveSync")
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 85061f0368...6c8d0b0c32
417
src/main.ts
417
src/main.ts
@@ -1,6 +1,6 @@
|
||||
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 { 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 } 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, 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 { delay, getDocData, isDocContentSame } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
@@ -9,7 +9,7 @@ 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 } from "./utils";
|
||||
import { applyPatch, clearAllPeriodic, clearAllTriggers, clearTrigger, disposeMemoObject, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, memoIfNotExist, memoObject, flattenObject, path2id, retrieveMemoObject, setTrigger, tryParseJSON, createFile, modifyFile, isValidPath } from "./utils";
|
||||
import { decrypt, encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
||||
|
||||
const isDebug = false;
|
||||
@@ -20,7 +20,7 @@ import { getGlobalStore, observeStores } from "./lib/src/store";
|
||||
import { lockStore, logMessageStore, logStore } from "./lib/src/stores";
|
||||
import { NewNotice, setNoticeClass, WrappedNotice } from "./lib/src/wrapper";
|
||||
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
|
||||
import { isPlainText, isValidPath, shouldBeIgnored } from "./lib/src/path";
|
||||
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
||||
import { runWithLock } from "./lib/src/lock";
|
||||
import { Semaphore } from "./lib/src/semaphore";
|
||||
import { JsonResolveModal } from "./JsonResolveModal";
|
||||
@@ -32,6 +32,8 @@ 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
|
||||
@@ -301,91 +303,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
Logger(`Checking expired file history done`);
|
||||
}
|
||||
|
||||
async onload() {
|
||||
logStore.subscribe(e => this.addLog(e.message, e.level, e.key));
|
||||
Logger("loading plugin");
|
||||
//@ts-ignore
|
||||
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
|
||||
//@ts-ignore
|
||||
const packageVersion: string = PACKAGE_VERSION || "0.0.0";
|
||||
|
||||
|
||||
Logger(`Self-hosted LiveSync v${manifestVersion} ${packageVersion} `);
|
||||
const lsKey = "obsidian-live-sync-ver" + this.getVaultName();
|
||||
const last_version = localStorage.getItem(lsKey);
|
||||
await this.loadSettings();
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
if (lastVersion > this.settings.lastReadUpdates) {
|
||||
Logger("Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
//@ts-ignore
|
||||
if (this.app.isMobile) {
|
||||
this.isMobile = true;
|
||||
this.settings.disableRequestURI = true;
|
||||
}
|
||||
if (last_version && Number(last_version) < VER) {
|
||||
this.settings.liveSync = false;
|
||||
this.settings.syncOnSave = false;
|
||||
this.settings.syncOnStart = false;
|
||||
this.settings.syncOnFileOpen = false;
|
||||
this.settings.syncAfterMerge = false;
|
||||
this.settings.periodicReplication = false;
|
||||
this.settings.versionUpFlash = "Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.";
|
||||
this.saveSettings();
|
||||
}
|
||||
localStorage.setItem(lsKey, `${VER}`);
|
||||
await this.openDatabase();
|
||||
|
||||
addIcon(
|
||||
"replicate",
|
||||
`<g transform="matrix(1.15 0 0 1.15 -8.31 -9.52)" fill="currentColor" fill-rule="evenodd">
|
||||
<path d="m85 22.2c-0.799-4.74-4.99-8.37-9.88-8.37-0.499 0-1.1 0.101-1.6 0.101-2.4-3.03-6.09-4.94-10.3-4.94-6.09 0-11.2 4.14-12.8 9.79-5.59 1.11-9.78 6.05-9.78 12 0 6.76 5.39 12.2 12 12.2h29.9c5.79 0 10.1-4.74 10.1-10.6 0-4.84-3.29-8.88-7.68-10.2zm-2.99 14.7h-29.5c-2.3-0.202-4.29-1.51-5.29-3.53-0.899-2.12-0.699-4.54 0.698-6.46 1.2-1.61 2.99-2.52 4.89-2.52 0.299 0 0.698 0 0.998 0.101l1.8 0.303v-2.02c0-3.63 2.4-6.76 5.89-7.57 0.599-0.101 1.2-0.202 1.8-0.202 2.89 0 5.49 1.62 6.79 4.24l0.598 1.21 1.3-0.504c0.599-0.202 1.3-0.303 2-0.303 1.3 0 2.5 0.404 3.59 1.11 1.6 1.21 2.6 3.13 2.6 5.15v1.61h2c2.6 0 4.69 2.12 4.69 4.74-0.099 2.52-2.2 4.64-4.79 4.64z"/>
|
||||
<path d="m53.2 49.2h-41.6c-1.8 0-3.2 1.4-3.2 3.2v28.6c0 1.8 1.4 3.2 3.2 3.2h15.8v4h-7v6h24v-6h-7v-4h15.8c1.8 0 3.2-1.4 3.2-3.2v-28.6c0-1.8-1.4-3.2-3.2-3.2zm-2.8 29h-36v-23h36z"/>
|
||||
<path d="m73 49.2c1.02 1.29 1.53 2.97 1.53 4.56 0 2.97-1.74 5.65-4.39 7.04v-4.06l-7.46 7.33 7.46 7.14v-4.06c7.66-1.98 12.2-9.61 10-17-0.102-0.297-0.205-0.595-0.307-0.892z"/>
|
||||
<path d="m24.1 43c-0.817-0.991-1.53-2.97-1.53-4.56 0-2.97 1.74-5.65 4.39-7.04v4.06l7.46-7.33-7.46-7.14v4.06c-7.66 1.98-12.2 9.61-10 17 0.102 0.297 0.205 0.595 0.307 0.892z"/>
|
||||
</g>`
|
||||
);
|
||||
addIcon(
|
||||
"view-log",
|
||||
`<g transform="matrix(1.28 0 0 1.28 -131 -411)" fill="currentColor" fill-rule="evenodd">
|
||||
<path d="m103 330h76v12h-76z"/>
|
||||
<path d="m106 346v44h70v-44zm45 16h-20v-8h20z"/>
|
||||
</g>`
|
||||
);
|
||||
this.addRibbonIcon("replicate", "Replicate", async () => {
|
||||
await this.replicate(true);
|
||||
});
|
||||
|
||||
this.addRibbonIcon("view-log", "Show log", () => {
|
||||
new LogDisplayModal(this.app, this).open();
|
||||
});
|
||||
|
||||
this.statusBar = this.addStatusBarItem();
|
||||
this.statusBar.addClass("syncstatusbar");
|
||||
|
||||
this.statusBar2 = this.addStatusBarItem();
|
||||
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);
|
||||
// this.registerWatchEvents();
|
||||
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
||||
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
async onLayoutReady() {
|
||||
this.registerFileWatchEvents();
|
||||
if (this.localDatabase.isReady)
|
||||
try {
|
||||
@@ -415,7 +333,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.settings.suspendFileWatching = false;
|
||||
await this.saveSettings();
|
||||
// @ts-ignore
|
||||
this.app.commands.executeCommandById("app:reload");
|
||||
this.app.commands.executeCommandById("app:reload")
|
||||
}
|
||||
} 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);
|
||||
@@ -429,9 +347,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.settings.suspendFileWatching = false;
|
||||
await this.saveSettings();
|
||||
// @ts-ignore
|
||||
this.app.commands.executeCommandById("app:reload");
|
||||
this.app.commands.executeCommandById("app:reload")
|
||||
}
|
||||
} else {
|
||||
this.settings.writeLogToTheFile = true;
|
||||
await this.openDatabase();
|
||||
const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured.";
|
||||
Logger(warningMessage, LOG_LEVEL.NOTICE);
|
||||
@@ -456,12 +375,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
});
|
||||
const configURIBase = "obsidian://setuplivesync?settings=";
|
||||
this.addCommand({
|
||||
id: "livesync-copysetupuri",
|
||||
name: "Copy the setup URI",
|
||||
callback: async () => {
|
||||
}
|
||||
|
||||
async command_copySetupURI() {
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "");
|
||||
if (encryptingPassphrase === false) return;
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||
@@ -475,12 +391,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const uri = `${configURIBase}${encryptedSetting}`;
|
||||
await navigator.clipboard.writeText(uri);
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-copysetupurifull",
|
||||
name: "Copy the setup URI (Full)",
|
||||
callback: async () => {
|
||||
}
|
||||
async command_copySetupURIFull() {
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "");
|
||||
if (encryptingPassphrase === false) return;
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||
@@ -488,12 +400,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const uri = `${configURIBase}${encryptedSetting}`;
|
||||
await navigator.clipboard.writeText(uri);
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-opensetupuri",
|
||||
name: "Open the setup URI",
|
||||
callback: async () => {
|
||||
}
|
||||
async command_openSetupURI() {
|
||||
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
|
||||
if (setupURI === false) return;
|
||||
if (!setupURI.startsWith(`${configURIBase}`)) {
|
||||
@@ -502,10 +410,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
const config = decodeURIComponent(setupURI.substring(configURIBase.length));
|
||||
console.dir(config)
|
||||
await setupWizard(config);
|
||||
},
|
||||
});
|
||||
const setupWizard = async (confString: string) => {
|
||||
await this.setupWizard(config);
|
||||
}
|
||||
async setupWizard(confString: string) {
|
||||
try {
|
||||
const oldConf = JSON.parse(JSON.stringify(this.settings));
|
||||
const encryptingPassphrase = await askString(this.app, "Passphrase", "The passphrase to decrypt your setup URI", "");
|
||||
@@ -620,9 +527,115 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
} catch (ex) {
|
||||
Logger("Couldn't parse or decrypt configuration uri.", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
};
|
||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||
await setupWizard(conf.settings);
|
||||
}
|
||||
async onload() {
|
||||
logStore.subscribe(e => this.addLog(e.message, e.level, e.key));
|
||||
Logger("loading plugin");
|
||||
//@ts-ignore
|
||||
const manifestVersion: string = MANIFEST_VERSION || "0.0.0";
|
||||
//@ts-ignore
|
||||
const packageVersion: string = PACKAGE_VERSION || "0.0.0";
|
||||
|
||||
|
||||
Logger(`Self-hosted LiveSync v${manifestVersion} ${packageVersion} `);
|
||||
const lsKey = "obsidian-live-sync-ver" + this.getVaultName();
|
||||
const last_version = localStorage.getItem(lsKey);
|
||||
await this.loadSettings();
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
if (lastVersion > this.settings.lastReadUpdates) {
|
||||
Logger("Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
//@ts-ignore
|
||||
if (this.app.isMobile) {
|
||||
this.isMobile = true;
|
||||
this.settings.disableRequestURI = true;
|
||||
}
|
||||
if (last_version && Number(last_version) < VER) {
|
||||
this.settings.liveSync = false;
|
||||
this.settings.syncOnSave = false;
|
||||
this.settings.syncOnStart = false;
|
||||
this.settings.syncOnFileOpen = false;
|
||||
this.settings.syncAfterMerge = false;
|
||||
this.settings.periodicReplication = false;
|
||||
this.settings.versionUpFlash = "Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.";
|
||||
this.saveSettings();
|
||||
}
|
||||
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);
|
||||
|
||||
this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000);
|
||||
this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000);
|
||||
this.setupWizard = this.setupWizard.bind(this);
|
||||
|
||||
this.statusBar = this.addStatusBarItem();
|
||||
this.statusBar.addClass("syncstatusbar");
|
||||
|
||||
this.statusBar2 = this.addStatusBarItem();
|
||||
|
||||
|
||||
addIcon(
|
||||
"replicate",
|
||||
`<g transform="matrix(1.15 0 0 1.15 -8.31 -9.52)" fill="currentColor" fill-rule="evenodd">
|
||||
<path d="m85 22.2c-0.799-4.74-4.99-8.37-9.88-8.37-0.499 0-1.1 0.101-1.6 0.101-2.4-3.03-6.09-4.94-10.3-4.94-6.09 0-11.2 4.14-12.8 9.79-5.59 1.11-9.78 6.05-9.78 12 0 6.76 5.39 12.2 12 12.2h29.9c5.79 0 10.1-4.74 10.1-10.6 0-4.84-3.29-8.88-7.68-10.2zm-2.99 14.7h-29.5c-2.3-0.202-4.29-1.51-5.29-3.53-0.899-2.12-0.699-4.54 0.698-6.46 1.2-1.61 2.99-2.52 4.89-2.52 0.299 0 0.698 0 0.998 0.101l1.8 0.303v-2.02c0-3.63 2.4-6.76 5.89-7.57 0.599-0.101 1.2-0.202 1.8-0.202 2.89 0 5.49 1.62 6.79 4.24l0.598 1.21 1.3-0.504c0.599-0.202 1.3-0.303 2-0.303 1.3 0 2.5 0.404 3.59 1.11 1.6 1.21 2.6 3.13 2.6 5.15v1.61h2c2.6 0 4.69 2.12 4.69 4.74-0.099 2.52-2.2 4.64-4.79 4.64z"/>
|
||||
<path d="m53.2 49.2h-41.6c-1.8 0-3.2 1.4-3.2 3.2v28.6c0 1.8 1.4 3.2 3.2 3.2h15.8v4h-7v6h24v-6h-7v-4h15.8c1.8 0 3.2-1.4 3.2-3.2v-28.6c0-1.8-1.4-3.2-3.2-3.2zm-2.8 29h-36v-23h36z"/>
|
||||
<path d="m73 49.2c1.02 1.29 1.53 2.97 1.53 4.56 0 2.97-1.74 5.65-4.39 7.04v-4.06l-7.46 7.33 7.46 7.14v-4.06c7.66-1.98 12.2-9.61 10-17-0.102-0.297-0.205-0.595-0.307-0.892z"/>
|
||||
<path d="m24.1 43c-0.817-0.991-1.53-2.97-1.53-4.56 0-2.97 1.74-5.65 4.39-7.04v4.06l7.46-7.33-7.46-7.14v4.06c-7.66 1.98-12.2 9.61-10 17 0.102 0.297 0.205 0.595 0.307 0.892z"/>
|
||||
</g>`
|
||||
);
|
||||
addIcon(
|
||||
"view-log",
|
||||
`<g transform="matrix(1.28 0 0 1.28 -131 -411)" fill="currentColor" fill-rule="evenodd">
|
||||
<path d="m103 330h76v12h-76z"/>
|
||||
<path d="m106 346v44h70v-44zm45 16h-20v-8h20z"/>
|
||||
</g>`
|
||||
);
|
||||
this.addRibbonIcon("replicate", "Replicate", async () => {
|
||||
await this.replicate(true);
|
||||
});
|
||||
|
||||
this.addRibbonIcon("view-log", "Show log", () => {
|
||||
new LogDisplayModal(this.app, this).open();
|
||||
});
|
||||
|
||||
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
||||
|
||||
this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this));
|
||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => await this.setupWizard(conf.settings));
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-copysetupuri",
|
||||
name: "Copy the setup URI",
|
||||
callback: this.command_copySetupURI.bind(this),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-copysetupurifull",
|
||||
name: "Copy the setup URI (Full)",
|
||||
callback: this.command_copySetupURIFull.bind(this),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-opensetupuri",
|
||||
name: "Open the setup URI",
|
||||
callback: this.command_openSetupURI.bind(this),
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-replicate",
|
||||
@@ -995,6 +1008,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
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;
|
||||
@@ -1290,11 +1306,24 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return;
|
||||
}
|
||||
const vaultName = this.getVaultName();
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const now = new Date();
|
||||
const timestamp = now.toLocaleString();
|
||||
const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
||||
if (message instanceof Error) {
|
||||
// debugger;
|
||||
}
|
||||
const newMessage = timestamp + "->" + messageContent;
|
||||
|
||||
console.log(vaultName + ":" + newMessage);
|
||||
if (this.settings.writeLogToTheFile) {
|
||||
const time = now.toISOString().split("T")[0];
|
||||
const logDate = `${PREFIXMD_LOGFILE}${time}.md`;
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(logDate));
|
||||
if (!file) {
|
||||
this.app.vault.adapter.append(normalizePath(logDate), "```\n");
|
||||
}
|
||||
this.app.vault.adapter.append(normalizePath(logDate), vaultName + ":" + newMessage + "\n");
|
||||
}
|
||||
logMessageStore.apply(e => [...e, newMessage].slice(-100));
|
||||
this.setStatusBarText(null, messageContent.substring(0, 30));
|
||||
|
||||
@@ -1358,60 +1387,63 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
async doc2storage_create(docEntry: EntryBody, force?: boolean) {
|
||||
async doc2storage(docEntry: EntryBody, file?: TFile, force?: boolean) {
|
||||
const mode = file == undefined ? "create" : "modify";
|
||||
|
||||
const pathSrc = id2path(docEntry._id);
|
||||
if (shouldBeIgnored(pathSrc)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isTargetFile(pathSrc)) return;
|
||||
if (docEntry._deleted || docEntry.deleted) {
|
||||
// This occurs not only when files are deleted, but also when conflicts are resolved.
|
||||
// We have to check no other revisions are left.
|
||||
const lastDocs = await this.localDatabase.getDBEntry(pathSrc);
|
||||
if (lastDocs === false) {
|
||||
await this.deleteVaultItem(file);
|
||||
} else {
|
||||
// it perhaps delete some revisions.
|
||||
// may be we have to reload this
|
||||
await this.pullFile(pathSrc, null, true);
|
||||
Logger(`delete skipped:${lastDocs._id}`, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const localMtime = ~~(file?.stat?.mtime || 0 / 1000);
|
||||
const docMtime = ~~(docEntry.mtime / 1000);
|
||||
|
||||
const doc = await this.localDatabase.getDBEntry(pathSrc, { rev: docEntry._rev });
|
||||
if (doc === false) return;
|
||||
const msg = `DB -> STORAGE (create${force ? ",force" : ""},${doc.datatype}) `;
|
||||
const path = id2path(doc._id);
|
||||
if (doc.datatype == "newnote") {
|
||||
const bin = base64ToArrayBuffer(doc.data);
|
||||
if (bin != null) {
|
||||
const msg = `DB -> STORAGE (${mode}${force ? ",force" : ""},${doc.datatype}) `;
|
||||
if (doc.datatype != "newnote" && doc.datatype != "plain") {
|
||||
Logger(msg + "ERROR, Invalid datatype: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
if (!force && localMtime >= docMtime) return;
|
||||
if (!isValidPath(path)) {
|
||||
Logger(msg + "ERROR, invalid path: " + path, LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
await this.ensureDirectory(path);
|
||||
const writeData = doc.datatype == "newnote" ? base64ToArrayBuffer(doc.data) : getDocData(doc.data);
|
||||
await this.ensureDirectoryEx(path);
|
||||
try {
|
||||
const newFile = await this.app.vault.createBinary(normalizePath(path), bin, {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
let outFile;
|
||||
if (mode == "create") {
|
||||
outFile = await createFile(normalizePath(path), writeData, { ctime: doc.ctime, mtime: doc.mtime, });
|
||||
} else {
|
||||
await modifyFile(file, writeData, { ctime: doc.ctime, mtime: doc.mtime });
|
||||
outFile = getAbstractFileByPath(file.path) as TFile;
|
||||
}
|
||||
Logger(msg + path);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
touch(outFile);
|
||||
this.app.vault.trigger(mode, outFile);
|
||||
|
||||
} catch (ex) {
|
||||
Logger(msg + "ERROR, Could not write: " + path, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
}
|
||||
} else if (doc.datatype == "plain") {
|
||||
if (!isValidPath(path)) {
|
||||
Logger(msg + "ERROR, invalid path: " + path, LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
await this.ensureDirectory(path);
|
||||
try {
|
||||
const newFile = await this.app.vault.create(normalizePath(path), getDocData(doc.data), {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
Logger(msg + path);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
} catch (ex) {
|
||||
Logger(msg + "ERROR, Could not create: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
} else {
|
||||
Logger(msg + "ERROR, Could not parse: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteVaultItem(file: TFile | TFolder) {
|
||||
if (file instanceof TFile) {
|
||||
@@ -1433,79 +1465,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
async doc2storage_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
|
||||
const pathSrc = id2path(docEntry._id);
|
||||
if (shouldBeIgnored(pathSrc)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isTargetFile(pathSrc)) return;
|
||||
if (docEntry._deleted || docEntry.deleted) {
|
||||
// This occurs not only when files are deleted, but also when conflicts are resolved.
|
||||
// We have to check no other revisions are left.
|
||||
const lastDocs = await this.localDatabase.getDBEntry(pathSrc);
|
||||
if (lastDocs === false) {
|
||||
await this.deleteVaultItem(file);
|
||||
} else {
|
||||
// it perhaps delete some revisions.
|
||||
// may be we have to reload this
|
||||
await this.pullFile(pathSrc, null, true);
|
||||
Logger(`delete skipped:${lastDocs._id}`, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const localMtime = ~~(file.stat.mtime / 1000);
|
||||
const docMtime = ~~(docEntry.mtime / 1000);
|
||||
if (localMtime < docMtime || force) {
|
||||
const doc = await this.localDatabase.getDBEntry(pathSrc);
|
||||
if (doc === false) return;
|
||||
const msg = `DB -> STORAGE (modify${force ? ",force" : ""},${doc.datatype}) `;
|
||||
const path = id2path(doc._id);
|
||||
if (doc.datatype == "newnote") {
|
||||
const bin = base64ToArrayBuffer(doc.data);
|
||||
if (bin != null) {
|
||||
if (!isValidPath(path)) {
|
||||
Logger(msg + "ERROR, invalid path: " + path, LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
Logger(msg + path);
|
||||
const xf = getAbstractFileByPath(file.path) as TFile;
|
||||
touch(xf);
|
||||
this.app.vault.trigger("modify", xf);
|
||||
} catch (ex) {
|
||||
Logger(msg + "ERROR, Could not write: " + path, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
}
|
||||
} else if (doc.datatype == "plain") {
|
||||
if (!isValidPath(path)) {
|
||||
Logger(msg + "ERROR, invalid path: " + path, LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
await this.ensureDirectory(path);
|
||||
try {
|
||||
await this.app.vault.modify(file, getDocData(doc.data), { ctime: doc.ctime, mtime: doc.mtime });
|
||||
Logger(msg + path);
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||
const xf = getAbstractFileByPath(file.path) as TFile;
|
||||
touch(xf);
|
||||
this.app.vault.trigger("modify", xf);
|
||||
} catch (ex) {
|
||||
Logger(msg + "ERROR, Could not write: " + path, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
} else {
|
||||
Logger(msg + "ERROR, Could not parse: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
} else if (localMtime > docMtime) {
|
||||
// newer local file.
|
||||
// ?
|
||||
} else {
|
||||
//Nothing have to op.
|
||||
//eq.case
|
||||
}
|
||||
}
|
||||
|
||||
queuedEntries: EntryBody[] = [];
|
||||
handleDBChanged(change: EntryBody) {
|
||||
@@ -1544,7 +1503,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return;
|
||||
}
|
||||
const doc = change;
|
||||
await this.doc2storage_create(doc);
|
||||
await this.doc2storage(doc);
|
||||
} else if (targetFile instanceof TFile) {
|
||||
const doc = change;
|
||||
const file = targetFile;
|
||||
@@ -1562,12 +1521,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return false;
|
||||
}
|
||||
if (this.settings.writeDocumentsIfConflicted) {
|
||||
await this.doc2storage_modify(doc, file);
|
||||
await this.doc2storage(doc, file);
|
||||
queueConflictCheck();
|
||||
} else {
|
||||
const d = await this.localDatabase.getDBEntryMeta(id2path(change._id), { conflicts: true }, true);
|
||||
if (d && !d._conflicts) {
|
||||
await this.doc2storage_modify(doc, file);
|
||||
await this.doc2storage(doc, file);
|
||||
} else {
|
||||
if (!queueConflictCheck()) {
|
||||
Logger(`${id2path(change._id)} is conflicted, write to the storage has been pended.`, LOG_LEVEL.NOTICE);
|
||||
@@ -2686,7 +2645,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger(`${filename} Skipped`);
|
||||
return;
|
||||
}
|
||||
await this.doc2storage_create(doc, force);
|
||||
await this.doc2storage(doc, undefined, force);
|
||||
} else if (targetFile instanceof TFile) {
|
||||
//normal case
|
||||
const file = targetFile;
|
||||
@@ -2695,7 +2654,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger(`${filename} Skipped`);
|
||||
return;
|
||||
}
|
||||
await this.doc2storage_modify(doc, file, force);
|
||||
await this.doc2storage(doc, file, force);
|
||||
} else {
|
||||
Logger(`target files:${filename} is exists as the folder`);
|
||||
//something went wrong..
|
||||
@@ -2737,7 +2696,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger(`${storageMtime} < ${docMtime}`);
|
||||
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
||||
if (docx != false) {
|
||||
await this.doc2storage_modify(docx, file);
|
||||
await this.doc2storage(docx, file);
|
||||
} else {
|
||||
Logger("STORAGE <- DB :" + file.path + " Skipped");
|
||||
}
|
||||
|
||||
35
src/utils.ts
35
src/utils.ts
@@ -1,6 +1,8 @@
|
||||
import { normalizePath } from "obsidian";
|
||||
import { DataWriteOptions, normalizePath, TFile, Platform } from "obsidian";
|
||||
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid } from "./lib/src/path";
|
||||
|
||||
import { path2id_base, id2path_base } from "./lib/src/path";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { LOG_LEVEL } from "./lib/src/types";
|
||||
|
||||
// For backward compatibility, using the path for determining id.
|
||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
@@ -260,3 +262,32 @@ export function flattenObject(obj: Record<string | number | symbol, any>, path:
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function modifyFile(file: TFile, data: string | ArrayBuffer, options?: DataWriteOptions) {
|
||||
if (typeof (data) === "string") {
|
||||
return app.vault.modify(file, data, options);
|
||||
} else {
|
||||
return app.vault.modifyBinary(file, data, options);
|
||||
}
|
||||
}
|
||||
export function createFile(path: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise<TFile> {
|
||||
if (typeof (data) === "string") {
|
||||
return app.vault.create(path, data, options);
|
||||
} else {
|
||||
return app.vault.createBinary(path, data, options);
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidPath(filename: string) {
|
||||
if (Platform.isDesktop) {
|
||||
// if(Platform.isMacOS) return isValidFilenameInDarwin(filename);
|
||||
if (process.platform == "darwin") return isValidFilenameInDarwin(filename);
|
||||
if (process.platform == "linux") return isValidFilenameInLinux(filename);
|
||||
return isValidFilenameInWidows(filename);
|
||||
}
|
||||
if (Platform.isAndroidApp) return isValidFilenameInAndroid(filename);
|
||||
if (Platform.isIosApp) return isValidFilenameInDarwin(filename);
|
||||
//Fallback
|
||||
Logger("Could not determine platform for checking filename", LOG_LEVEL.VERBOSE);
|
||||
return isValidFilenameInWidows(filename);
|
||||
}
|
||||
Reference in New Issue
Block a user