- Improved:

- Plugins and their settings no longer need scanning if changes are monitored.
  - Now synchronising plugins and their settings are performed parallelly and faster.
  - We can place `redflag2.md` to rebuild the database automatically while the boot sequence.
- Experimental:
  - We can use a new adapter on PouchDB. This will make us smoother.
    - Note: Not compatible with the older version.
- Fixed:
  - The default batch size is smaller again.
  - Plugins and their setting can be synchronised again.
  - Hidden files and plugins are correctly scanned while rebuilding.
  - Files with the name started `_` are also being performed conflict-checking.
This commit is contained in:
vorotamoroz
2023-01-25 20:53:20 +09:00
parent 71a80cacc3
commit ec8d9785ed
6 changed files with 223 additions and 110 deletions

29
package-lock.json generated
View File

@@ -33,6 +33,7 @@
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"pouchdb-adapter-http": "^8.0.0", "pouchdb-adapter-http": "^8.0.0",
"pouchdb-adapter-idb": "^8.0.0", "pouchdb-adapter-idb": "^8.0.0",
"pouchdb-adapter-indexeddb": "^8.0.0",
"pouchdb-core": "^8.0.0", "pouchdb-core": "^8.0.0",
"pouchdb-find": "^8.0.0", "pouchdb-find": "^8.0.0",
"pouchdb-mapreduce": "^8.0.0", "pouchdb-mapreduce": "^8.0.0",
@@ -2889,6 +2890,20 @@
"pouchdb-utils": "8.0.0" "pouchdb-utils": "8.0.0"
} }
}, },
"node_modules/pouchdb-adapter-indexeddb": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-adapter-indexeddb/-/pouchdb-adapter-indexeddb-8.0.0.tgz",
"integrity": "sha512-h+vMPspVF6s4IKzLSys7iGDlANWkow77hJV/MX6JIftrjj/QS5jShSzhGCAR9HpLtuAVwQQM+k4hQodGnoAGWw==",
"dev": true,
"dependencies": {
"pouchdb-adapter-utils": "8.0.0",
"pouchdb-binary-utils": "8.0.0",
"pouchdb-errors": "8.0.0",
"pouchdb-md5": "8.0.0",
"pouchdb-merge": "8.0.0",
"pouchdb-utils": "8.0.0"
}
},
"node_modules/pouchdb-adapter-utils": { "node_modules/pouchdb-adapter-utils": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-adapter-utils/-/pouchdb-adapter-utils-8.0.0.tgz", "resolved": "https://registry.npmjs.org/pouchdb-adapter-utils/-/pouchdb-adapter-utils-8.0.0.tgz",
@@ -5841,6 +5856,20 @@
"pouchdb-utils": "8.0.0" "pouchdb-utils": "8.0.0"
} }
}, },
"pouchdb-adapter-indexeddb": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-adapter-indexeddb/-/pouchdb-adapter-indexeddb-8.0.0.tgz",
"integrity": "sha512-h+vMPspVF6s4IKzLSys7iGDlANWkow77hJV/MX6JIftrjj/QS5jShSzhGCAR9HpLtuAVwQQM+k4hQodGnoAGWw==",
"dev": true,
"requires": {
"pouchdb-adapter-utils": "8.0.0",
"pouchdb-binary-utils": "8.0.0",
"pouchdb-errors": "8.0.0",
"pouchdb-md5": "8.0.0",
"pouchdb-merge": "8.0.0",
"pouchdb-utils": "8.0.0"
}
},
"pouchdb-adapter-utils": { "pouchdb-adapter-utils": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-adapter-utils/-/pouchdb-adapter-utils-8.0.0.tgz", "resolved": "https://registry.npmjs.org/pouchdb-adapter-utils/-/pouchdb-adapter-utils-8.0.0.tgz",

View File

@@ -30,6 +30,7 @@
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"pouchdb-adapter-http": "^8.0.0", "pouchdb-adapter-http": "^8.0.0",
"pouchdb-adapter-idb": "^8.0.0", "pouchdb-adapter-idb": "^8.0.0",
"pouchdb-adapter-indexeddb": "^8.0.0",
"pouchdb-core": "^8.0.0", "pouchdb-core": "^8.0.0",
"pouchdb-find": "^8.0.0", "pouchdb-find": "^8.0.0",
"pouchdb-mapreduce": "^8.0.0", "pouchdb-mapreduce": "^8.0.0",

View File

@@ -3,7 +3,7 @@ import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB.js";
import { LocalPouchDBBase } from "./lib/src/LocalPouchDBBase.js"; import { LocalPouchDBBase } from "./lib/src/LocalPouchDBBase.js";
import { Logger } from "./lib/src/logger.js"; import { Logger } from "./lib/src/logger.js";
import { PouchDB } from "./lib/src/pouchdb-browser.js"; import { PouchDB } from "./lib/src/pouchdb-browser.js";
import { EntryDoc, LOG_LEVEL } from "./lib/src/types.js"; import { EntryDoc, LOG_LEVEL, ObsidianLiveSyncSettings } from "./lib/src/types.js";
import { enableEncryption } from "./lib/src/utils_couchdb.js"; import { enableEncryption } from "./lib/src/utils_couchdb.js";
import { isCloudantURI, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb.js"; import { isCloudantURI, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb.js";
import { id2path, path2id } from "./utils.js"; import { id2path, path2id } from "./utils.js";
@@ -11,6 +11,7 @@ import { id2path, path2id } from "./utils.js";
export class LocalPouchDB extends LocalPouchDBBase { export class LocalPouchDB extends LocalPouchDBBase {
kvDB: KeyValueDatabase; kvDB: KeyValueDatabase;
settings: ObsidianLiveSyncSettings;
id2path(filename: string): string { id2path(filename: string): string {
return id2path(filename); return id2path(filename);
} }
@@ -18,6 +19,10 @@ export class LocalPouchDB extends LocalPouchDBBase {
return path2id(filename); return path2id(filename);
} }
CreatePouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> { CreatePouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
if (this.settings.useIndexedDBAdapter) {
options.adapter = "indexeddb";
return new PouchDB(name + "-indexeddb", options);
}
return new PouchDB(name, options); return new PouchDB(name, options);
} }
beforeOnUnload(): void { beforeOnUnload(): void {

View File

@@ -817,6 +817,23 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
); );
containerLocalDatabaseEl.createEl("h3", {
text: sanitizeHTMLToDom(`Experimental`),
cls: "wizardHidden"
});
new Setting(containerLocalDatabaseEl)
.setName("Use new adapter")
.setDesc("This option is not compatible with a database made by older versions. Changing this configuration will fetch the remote database again.")
.setClass("wizardHidden")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.useIndexedDBAdapter).onChange(async (value) => {
this.plugin.settings.useIndexedDBAdapter = value;
await this.plugin.saveSettings();
await rebuildDB("localOnly");
})
);
addScreenElement("10", containerLocalDatabaseEl); addScreenElement("10", containerLocalDatabaseEl);
const containerGeneralSettingsEl = containerEl.createDiv(); const containerGeneralSettingsEl = containerEl.createDiv();
containerGeneralSettingsEl.createEl("h3", { text: "General Settings" }); containerGeneralSettingsEl.createEl("h3", { text: "General Settings" });
@@ -866,6 +883,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}); });
text.inputEl.setAttribute("type", "number"); text.inputEl.setAttribute("type", "number");
}); });
new Setting(containerGeneralSettingsEl)
.setName("Monitor changes to hidden files and plugin")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.watchInternalFileChanges).onChange(async (value) => {
this.plugin.settings.watchInternalFileChanges = value;
await this.plugin.saveSettings();
})
);
addScreenElement("20", containerGeneralSettingsEl); addScreenElement("20", containerGeneralSettingsEl);
@@ -1039,7 +1064,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
); );
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Sync hidden files") .setName("Sync hidden files")
.addToggle((toggle) => .addToggle((toggle) =>
@@ -1048,14 +1072,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerSyncSettingEl)
.setName("Monitor changes to internal files")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.watchInternalFileChanges).onChange(async (value) => {
this.plugin.settings.watchInternalFileChanges = value;
await this.plugin.saveSettings();
})
);
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Scan for hidden files before replication") .setName("Scan for hidden files before replication")
.setDesc("This configuration will be ignored if monitoring changes is enabled.") .setDesc("This configuration will be ignored if monitoring changes is enabled.")
@@ -1551,7 +1568,7 @@ ${stringifyYaml(pluginConfig)}`;
new Setting(containerPluginSettings) new Setting(containerPluginSettings)
.setName("Scan plugins periodically") .setName("Scan plugins periodically")
.setDesc("Scan plugins every 1 minute.") .setDesc("Scan plugins every 1 minute. This configuration will be ignored if monitoring changes of hidden files has been enabled.")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => { toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
this.plugin.settings.autoSweepPluginsPeriodic = value; this.plugin.settings.autoSweepPluginsPeriodic = value;

Submodule src/lib updated: 14fecd2411...2567497fa6

View File

@@ -1,6 +1,6 @@
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App } from "obsidian"; import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, App } from "obsidian";
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry, SALT_OF_PASSPHRASE, ConfigPassphraseStore, CouchDBConnection } 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 } 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, FileInfo } from "./types";
import { getDocData, isDocContentSame } from "./lib/src/utils"; import { getDocData, isDocContentSame } from "./lib/src/utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
@@ -41,19 +41,22 @@ function getAbstractFileByPath(path: string): TAbstractFile | null {
return app.vault.getAbstractFileByPath(path); 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 * returns is internal chunk of file
* @param str ID * @param str ID
* @returns * @returns
*/ */
function isInternalChunk(str: string): boolean { function isInternalMetadata(str: string): boolean {
return str.startsWith(ICHeader); return str.startsWith(ICHeader);
} }
function id2filenameInternalChunk(str: string): string { function id2filenameInternalMetadata(str: string): string {
return str.substring(ICHeaderLength); return str.substring(ICHeaderLength);
} }
function filename2idInternalChunk(str: string): string { function filename2idInternalMetadata(str: string): string {
return ICHeader + str; return ICHeader + str;
} }
@@ -66,7 +69,7 @@ function isChunk(str: string): boolean {
const PSCHeader = "ps:"; const PSCHeader = "ps:";
const PSCHeaderEnd = "ps;"; const PSCHeaderEnd = "ps;";
function isPluginChunk(str: string): boolean { function isPluginMetadata(str: string): boolean {
return str.startsWith(PSCHeader); return str.startsWith(PSCHeader);
} }
@@ -152,6 +155,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
return false; return false;
} }
isRedFlag2Raised(): boolean {
const redflag = getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG2));
if (redflag != null) {
return true;
}
return false;
}
async deleteRedFlag2() {
const redflag = getAbstractFileByPath(normalizePath(FLAGMD_REDFLAG2));
if (redflag != null) {
await app.vault.delete(redflag, true);
}
}
showHistory(file: TFile | string) { showHistory(file: TFile | string) {
if (!this.settings.useHistory) { if (!this.settings.useHistory) {
@@ -199,8 +215,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
for (const row of docs.rows) { for (const row of docs.rows) {
const doc = row.doc; const doc = row.doc;
nextKey = `${row.id}\u{10ffff}`; nextKey = `${row.id}\u{10ffff}`;
if (isChunk(nextKey)) {
// skip the chunk zone.
nextKey = CHeaderEnd;
}
if (!("_conflicts" in doc)) continue; if (!("_conflicts" in doc)) continue;
if (isInternalChunk(row.id)) continue; if (isInternalMetadata(row.id)) continue;
// We have to check also deleted files. // We have to check also deleted files.
// if (doc._deleted) continue; // if (doc._deleted) continue;
// if ("deleted" in doc && doc.deleted) continue; // if ("deleted" in doc && doc.deleted) continue;
@@ -208,10 +228,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
// const docId = doc._id.startsWith("i:") ? doc._id.substring("i:".length) : doc._id; // const docId = doc._id.startsWith("i:") ? doc._id.substring("i:".length) : doc._id;
notes.push({ path: id2path(doc._id), mtime: doc.mtime }); notes.push({ path: id2path(doc._id), mtime: doc.mtime });
} }
if (isChunk(nextKey)) {
// skip the chunk zone.
nextKey = CHeaderEnd;
}
} }
} while (nextKey != ""); } while (nextKey != "");
notes.sort((a, b) => b.mtime - a.mtime); notes.sort((a, b) => b.mtime - a.mtime);
@@ -222,7 +239,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
const target = await askSelectString(this.app, "File to view History", notesList); const target = await askSelectString(this.app, "File to view History", notesList);
if (target) { if (target) {
if (isInternalChunk(target)) { if (isInternalMetadata(target)) {
//NOP //NOP
} else { } else {
await this.showIfConflicted(target); await this.showIfConflicted(target);
@@ -358,7 +375,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.registerFileWatchEvents(); this.registerFileWatchEvents();
if (this.localDatabase.isReady) if (this.localDatabase.isReady)
try { try {
if (this.isRedFlagRaised()) { if (this.isRedFlagRaised() || this.isRedFlag2Raised()) {
this.settings.batchSave = false; this.settings.batchSave = false;
this.settings.liveSync = false; this.settings.liveSync = false;
this.settings.periodicReplication = false; this.settings.periodicReplication = false;
@@ -371,10 +388,21 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.settings.suspendFileWatching = true; this.settings.suspendFileWatching = true;
this.settings.syncInternalFiles = false; this.settings.syncInternalFiles = false;
await this.saveSettings(); await this.saveSettings();
await this.openDatabase(); if (this.isRedFlag2Raised()) {
const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured."; Logger(`${FLAGMD_REDFLAG2} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`, LOG_LEVEL.NOTICE);
Logger(warningMessage, LOG_LEVEL.NOTICE); await this.resetLocalDatabase();
this.setStatusBarText(warningMessage); await this.initializeDatabase(true);
await this.markRemoteLocked();
await this.tryResetRemoteDatabase();
await this.markRemoteLocked();
await this.replicateAllToServer(true);
await this.deleteRedFlag2();
} else {
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);
this.setStatusBarText(warningMessage);
}
} else { } else {
if (this.settings.suspendFileWatching) { if (this.settings.suspendFileWatching) {
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE); Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE);
@@ -714,7 +742,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async openDatabase() { async openDatabase() {
if (this.localDatabase != null) { if (this.localDatabase != null) {
this.localDatabase.close(); await this.localDatabase.close();
} }
const vaultName = this.getVaultName(); const vaultName = this.getVaultName();
Logger("Open Database..."); Logger("Open Database...");
@@ -1142,7 +1170,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return; return;
} }
this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100); this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100);
const id = filename2idInternalChunk(path); const id = filename2idInternalMetadata(path);
const filesOnDB = await this.localDatabase.getDBEntryMeta(id); const filesOnDB = await this.localDatabase.getDBEntryMeta(id);
const dbMTime = ~~((filesOnDB && filesOnDB.mtime || 0) / 1000); const dbMTime = ~~((filesOnDB && filesOnDB.mtime || 0) / 1000);
@@ -1157,6 +1185,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.deleteInternalFileOnDatabase(path); await this.deleteInternalFileOnDatabase(path);
} else { } else {
await this.storeInternalFileToDatabase({ path: path, ...stat }); await this.storeInternalFileToDatabase({ path: path, ...stat });
const pluginDir = this.app.vault.configDir + "/plugins/";
const pluginFiles = ["manifest.json", "data.json", "style.css", "main.js"];
if (path.startsWith(pluginDir) && pluginFiles.some(e => path.endsWith(e)) && this.settings.usePluginSync) {
const pluginName = trimPrefix(path, pluginDir).split("/")[0]
await this.sweepPlugin(false, pluginName);
}
} }
} }
@@ -1554,9 +1588,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const now = new Date().getTime(); const now = new Date().getTime();
if (queue.missingChildren.length == 0) { if (queue.missingChildren.length == 0) {
queue.done = true; queue.done = true;
if (isInternalChunk(queue.entry._id)) { if (isInternalMetadata(queue.entry._id)) {
//system file //system file
const filename = id2path(id2filenameInternalChunk(queue.entry._id)); const filename = id2path(id2filenameInternalMetadata(queue.entry._id));
// await this.syncInternalFilesAndDatabase("pull", false, false, [filename]) // await this.syncInternalFilesAndDatabase("pull", false, false, [filename])
this.procInternalFile(filename); this.procInternalFile(filename);
} }
@@ -1597,7 +1631,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) { async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
if (!this.isTargetFile(id2path(doc._id))) return; if (!this.isTargetFile(id2path(doc._id))) return;
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary. const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
if ((!isInternalChunk(doc._id)) && skipOldFile) { if ((!isInternalMetadata(doc._id)) && skipOldFile) {
const info = getAbstractFileByPath(id2path(doc._id)); const info = getAbstractFileByPath(id2path(doc._id));
if (info && info instanceof TFile) { if (info && info instanceof TFile) {
@@ -1635,7 +1669,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//---> Sync //---> Sync
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> { async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
for (const change of docs) { for (const change of docs) {
if (isPluginChunk(change._id)) { if (isPluginMetadata(change._id)) {
if (this.settings.notifyPluginOrSettingUpdated) { if (this.settings.notifyPluginOrSettingUpdated) {
this.triggerCheckPluginUpdate(); this.triggerCheckPluginUpdate();
} }
@@ -1725,7 +1759,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
setPluginSweep() { setPluginSweep() {
if (this.settings.autoSweepPluginsPeriodic) { if (this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges) {
this.clearPluginSweep(); this.clearPluginSweep();
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicPluginSweep(), PERIODIC_PLUGIN_SWEEP * 1000); this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicPluginSweep(), PERIODIC_PLUGIN_SWEEP * 1000);
} }
@@ -1870,7 +1904,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
await this.applyBatchChange(); await this.applyBatchChange();
if (this.settings.autoSweepPlugins) { if (this.settings.autoSweepPlugins) {
await this.sweepPlugin(false); await this.sweepPlugin(showMessage);
} }
await this.loadQueuedFiles(); await this.loadQueuedFiles();
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication && !this.settings.watchInternalFileChanges) { if (this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication && !this.settings.watchInternalFileChanges) {
@@ -1885,6 +1919,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.localDatabase.isReady) { if (this.localDatabase.isReady) {
await this.syncAllFiles(showingNotice); await this.syncAllFiles(showingNotice);
} }
if (this.settings.syncInternalFiles) {
await this.syncInternalFilesAndDatabase("push", showingNotice);
}
if (this.settings.usePluginSync) {
await this.sweepPlugin(showingNotice);
}
this.isReady = true; this.isReady = true;
// run queued event once. // run queued event once.
await this.procFileEvent(true); await this.procFileEvent(true);
@@ -1929,7 +1969,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const wf = await this.localDatabase.localDatabase.allDocs(); const wf = await this.localDatabase.localDatabase.allDocs();
const filesDatabase = wf.rows.filter((e) => const filesDatabase = wf.rows.filter((e) =>
!isChunk(e.id) && !isChunk(e.id) &&
!isPluginChunk(e.id) && !isPluginMetadata(e.id) &&
e.id != "obsydian_livesync_version" && e.id != "obsydian_livesync_version" &&
e.id != "_design/replicate" e.id != "_design/replicate"
) )
@@ -1954,7 +1994,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
// const count = objects.length; // const count = objects.length;
Logger(procedureName); Logger(procedureName);
// let i = 0; // let i = 0;
const semaphore = Semaphore(10); const semaphore = Semaphore(25);
// Logger(`${procedureName} exec.`); // Logger(`${procedureName} exec.`);
if (!this.localDatabase.isReady) throw Error("Database is not ready!"); if (!this.localDatabase.isReady) throw Error("Database is not ready!");
@@ -2292,7 +2332,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
* @returns true -> resolved, false -> nothing to do, or check result. * @returns true -> resolved, false -> nothing to do, or check result.
*/ */
async getConflictedStatus(path: string): Promise<diff_check_result> { async getConflictedStatus(path: string): Promise<diff_check_result> {
const test = await this.localDatabase.getDBEntry(path, { conflicts: true }, false, false, true); const test = await this.localDatabase.getDBEntry(path, { conflicts: true, revs_info: true }, false, false, true);
if (test === false) return false; if (test === false) return false;
if (test == null) return false; if (test == null) return false;
if (!test._conflicts) return false; if (!test._conflicts) return false;
@@ -2302,7 +2342,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const conflictedRev = conflicts[0]; const conflictedRev = conflicts[0];
const conflictedRevNo = Number(conflictedRev.split("-")[0]); const conflictedRevNo = Number(conflictedRev.split("-")[0]);
//Search //Search
const revFrom = (await this.localDatabase.localDatabase.get(id2path(path), { revs_info: true })) as unknown as LoadedEntry & PouchDB.Core.GetMeta; const revFrom = (await this.localDatabase.localDatabase.get(path2id(path), { revs_info: true })) as unknown as LoadedEntry & PouchDB.Core.GetMeta;
const commonBase = revFrom._revs_info.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? ""; const commonBase = revFrom._revs_info.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? "";
let p = undefined; let p = undefined;
if (commonBase) { if (commonBase) {
@@ -2709,9 +2749,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return { plugins, allPlugins, thisDevicePlugins }; return { plugins, allPlugins, thisDevicePlugins };
} }
async sweepPlugin(showMessage = false) { async sweepPlugin(showMessage = false, specificPluginPath = "") {
if (!this.settings.usePluginSync) return; if (!this.settings.usePluginSync) return;
if (!this.localDatabase.isReady) return; if (!this.localDatabase.isReady) return;
// @ts-ignore
const pl = this.app.plugins;
const manifests: PluginManifest[] = Object.values(pl.manifests);
let specificPlugin = "";
if (specificPluginPath != "") {
specificPlugin = manifests.find(e => e.dir.endsWith("/" + specificPluginPath))?.id ?? "";
}
await runWithLock("sweepplugin", true, async () => { await runWithLock("sweepplugin", true, async () => {
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO; const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
if (!this.deviceAndVaultName) { if (!this.deviceAndVaultName) {
@@ -2721,71 +2768,81 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger("Scanning plugins", logLevel); Logger("Scanning plugins", logLevel);
const db = this.localDatabase.localDatabase; const db = this.localDatabase.localDatabase;
const oldDocs = await db.allDocs({ const oldDocs = await db.allDocs({
startkey: `ps:${this.deviceAndVaultName}-`, startkey: `ps:${this.deviceAndVaultName}-${specificPlugin}`,
endkey: `ps:${this.deviceAndVaultName}.`, endkey: `ps:${this.deviceAndVaultName}-${specificPlugin}\u{10ffff}`,
include_docs: true, include_docs: true,
}); });
// Logger("OLD DOCS.", LOG_LEVEL.VERBOSE); // Logger("OLD DOCS.", LOG_LEVEL.VERBOSE);
// sweep current plugin. // sweep current plugin.
// @ts-ignore
const pl = this.app.plugins; const procs = manifests.map(async m => {
const manifests: PluginManifest[] = Object.values(pl.manifests); const pluginDataEntryID = `ps:${this.deviceAndVaultName}-${m.id}`;
for (const m of manifests) { try {
Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE); if (specificPlugin && m.id != specificPlugin) {
const path = normalizePath(m.dir) + "/"; return;
const adapter = this.app.vault.adapter;
const files = ["manifest.json", "main.js", "styles.css", "data.json"];
const pluginData: { [key: string]: string } = {};
for (const file of files) {
const thePath = path + file;
if (await adapter.exists(thePath)) {
pluginData[file] = await adapter.read(thePath);
} }
} Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE);
let mtime = 0; const path = normalizePath(m.dir) + "/";
if (await adapter.exists(path + "/data.json")) { const adapter = this.app.vault.adapter;
mtime = (await adapter.stat(path + "/data.json")).mtime; const files = ["manifest.json", "main.js", "styles.css", "data.json"];
} const pluginData: { [key: string]: string } = {};
const p: PluginDataEntry = { for (const file of files) {
_id: `ps:${this.deviceAndVaultName}-${m.id}`, const thePath = path + file;
dataJson: pluginData["data.json"], if (await adapter.exists(thePath)) {
deviceVaultName: this.deviceAndVaultName, pluginData[file] = await adapter.read(thePath);
mainJs: pluginData["main.js"],
styleCss: pluginData["styles.css"],
manifest: m,
manifestJson: pluginData["manifest.json"],
mtime: mtime,
type: "plugin",
};
const d: LoadedEntry = {
_id: p._id,
data: JSON.stringify(p),
ctime: mtime,
mtime: mtime,
size: 0,
children: [],
datatype: "plain",
type: "plain"
};
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE);
await runWithLock("plugin-" + m.id, false, async () => {
const old = await this.localDatabase.getDBEntry(p._id, null, false, false);
if (old !== false) {
const oldData = { data: old.data, deleted: old._deleted };
const newData = { data: d.data, deleted: d._deleted };
if (JSON.stringify(oldData) == JSON.stringify(newData)) {
oldDocs.rows = oldDocs.rows.filter((e) => e.id != d._id);
Logger(`Nothing changed:${m.name}`);
return;
} }
} }
await this.localDatabase.putDBEntry(d); let mtime = 0;
oldDocs.rows = oldDocs.rows.filter((e) => e.id != d._id); if (await adapter.exists(path + "/data.json")) {
Logger(`Plugin saved:${m.name}`, logLevel); mtime = (await adapter.stat(path + "/data.json")).mtime;
}); }
const p: PluginDataEntry = {
_id: pluginDataEntryID,
dataJson: pluginData["data.json"],
deviceVaultName: this.deviceAndVaultName,
mainJs: pluginData["main.js"],
styleCss: pluginData["styles.css"],
manifest: m,
manifestJson: pluginData["manifest.json"],
mtime: mtime,
type: "plugin",
};
const d: LoadedEntry = {
_id: p._id,
data: JSON.stringify(p),
ctime: mtime,
mtime: mtime,
size: 0,
children: [],
datatype: "plain",
type: "plain"
};
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE);
await runWithLock("plugin-" + m.id, false, async () => {
const old = await this.localDatabase.getDBEntry(p._id, null, false, false);
if (old !== false) {
const oldData = { data: old.data, deleted: old._deleted };
const newData = { data: d.data, deleted: d._deleted };
if (isDocContentSame(oldData.data, newData.data) && oldData.deleted == newData.deleted) {
Logger(`Nothing changed:${m.name}`);
return;
}
}
await this.localDatabase.putDBEntry(d);
Logger(`Plugin saved:${m.name}`, logLevel);
});
} catch (ex) {
Logger(`Plugin save failed:${m.name}`, LOG_LEVEL.NOTICE)
} finally {
oldDocs.rows = oldDocs.rows.filter((e) => e.id != pluginDataEntryID);
}
//remove saved plugin data. //remove saved plugin data.
} }
Logger(`Deleting old plugins`, LOG_LEVEL.VERBOSE); );
await Promise.all(procs);
const delDocs = oldDocs.rows.map((e) => { const delDocs = oldDocs.rows.map((e) => {
// e.doc._deleted = true; // e.doc._deleted = true;
if (e.doc.type == "newnote" || e.doc.type == "plain") { if (e.doc.type == "newnote" || e.doc.type == "plain") {
@@ -2798,6 +2855,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
return e.doc; return e.doc;
}); });
Logger(`Deleting old plugin:(${delDocs.length})`, LOG_LEVEL.VERBOSE);
await db.bulkDocs(delDocs); await db.bulkDocs(delDocs);
Logger(`Scan plugin done.`, logLevel); Logger(`Scan plugin done.`, logLevel);
}); });
@@ -2927,11 +2985,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async storeInternalFileToDatabase(file: InternalFileInfo, forceWrite = false) { async storeInternalFileToDatabase(file: InternalFileInfo, forceWrite = false) {
const id = filename2idInternalChunk(path2id(file.path)); const id = filename2idInternalMetadata(path2id(file.path));
const contentBin = await this.app.vault.adapter.readBinary(file.path); const contentBin = await this.app.vault.adapter.readBinary(file.path);
const content = await arrayBufferToBase64(contentBin); const content = await arrayBufferToBase64(contentBin);
const mtime = file.mtime; const mtime = file.mtime;
await runWithLock("file-" + id, false, async () => { return await runWithLock("file-" + id, false, async () => {
const old = await this.localDatabase.getDBEntry(id, null, false, false); const old = await this.localDatabase.getDBEntry(id, null, false, false);
let saveData: LoadedEntry; let saveData: LoadedEntry;
if (old === false) { if (old === false) {
@@ -2947,7 +3005,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
type: "newnote", type: "newnote",
} }
} else { } else {
if (old.data == content && !forceWrite) { if (isDocContentSame(old.data, content) && !forceWrite) {
// Logger(`internal files STORAGE --> DB:${file.path}: Not changed`); // Logger(`internal files STORAGE --> DB:${file.path}: Not changed`);
return; return;
} }
@@ -2963,13 +3021,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
type: "newnote", type: "newnote",
} }
} }
await this.localDatabase.putDBEntry(saveData, true);
const ret = await this.localDatabase.putDBEntry(saveData, true);
Logger(`STORAGE --> DB:${file.path}: (hidden) Done`); Logger(`STORAGE --> DB:${file.path}: (hidden) Done`);
return ret;
}); });
} }
async deleteInternalFileOnDatabase(filename: string, forceWrite = false) { async deleteInternalFileOnDatabase(filename: string, forceWrite = false) {
const id = filename2idInternalChunk(path2id(filename)); const id = filename2idInternalMetadata(path2id(filename));
const mtime = new Date().getTime(); const mtime = new Date().getTime();
await runWithLock("file-" + id, false, async () => { await runWithLock("file-" + id, false, async () => {
const old = await this.localDatabase.getDBEntry(id, null, false, false) as InternalFileEntry | false; const old = await this.localDatabase.getDBEntry(id, null, false, false) as InternalFileEntry | false;
@@ -3026,7 +3086,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async extractInternalFileFromDatabase(filename: string, force = false) { async extractInternalFileFromDatabase(filename: string, force = false) {
const isExists = await this.app.vault.adapter.exists(filename); const isExists = await this.app.vault.adapter.exists(filename);
const id = filename2idInternalChunk(path2id(filename)); const id = filename2idInternalMetadata(path2id(filename));
return await runWithLock("file-" + id, false, async () => { return await runWithLock("file-" + id, false, async () => {
const fileOnDB = await this.localDatabase.getDBEntry(id, null, false, false) as false | LoadedEntry; const fileOnDB = await this.localDatabase.getDBEntry(id, null, false, false) as false | LoadedEntry;
@@ -3087,18 +3147,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
for (const row of docs.rows) { for (const row of docs.rows) {
const doc = row.doc; const doc = row.doc;
if (!("_conflicts" in doc)) continue; if (!("_conflicts" in doc)) continue;
if (isInternalChunk(row.id)) { if (isInternalMetadata(row.id)) {
await this.resolveConflictOnInternalFile(row.id); await this.resolveConflictOnInternalFile(row.id);
} }
} }
} }
async resolveConflictOnInternalFile(id: string): Promise<boolean> { async resolveConflictOnInternalFile(id: string): Promise<boolean> {
// Retrieve data // Retrieve data
const doc = await this.localDatabase.localDatabase.get(id, { conflicts: true }); const doc = await this.localDatabase.localDatabase.get(id, { conflicts: true });
// If there is no conflict, return with false. // If there is no conflict, return with false.
if (!("_conflicts" in doc)) return false; if (!("_conflicts" in doc)) return false;
if (doc._conflicts.length == 0) return false; if (doc._conflicts.length == 0) return false;
Logger(`Hidden file conflicted:${id2filenameInternalChunk(id)}`); Logger(`Hidden file conflicted:${id2filenameInternalMetadata(id)}`);
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0])); const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
const revA = doc._rev; const revA = doc._rev;
const revB = conflicts[0]; const revB = conflicts[0];
@@ -3112,7 +3173,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const result = await this.mergeObject(id, commonBase, doc._rev, conflictedRev); const result = await this.mergeObject(id, commonBase, doc._rev, conflictedRev);
if (result) { if (result) {
Logger(`Object merge:${id}`, LOG_LEVEL.INFO); Logger(`Object merge:${id}`, LOG_LEVEL.INFO);
const filename = id2filenameInternalChunk(id); const filename = id2filenameInternalMetadata(id);
const isExists = await this.app.vault.adapter.exists(filename); const isExists = await this.app.vault.adapter.exists(filename);
if (!isExists) { if (!isExists) {
await this.ensureDirectoryEx(filename); await this.ensureDirectoryEx(filename);
@@ -3137,7 +3198,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const delRev = mtimeA < mtimeB ? revA : revB; const delRev = mtimeA < mtimeB ? revA : revB;
// delete older one. // delete older one.
await this.localDatabase.localDatabase.remove(id, delRev); await this.localDatabase.localDatabase.remove(id, delRev);
Logger(`Older one has been deleted:${id2filenameInternalChunk(id)}`); Logger(`Older one has been deleted:${id2filenameInternalMetadata(id)}`);
// check the file again // check the file again
return this.resolveConflictOnInternalFile(id); return this.resolveConflictOnInternalFile(id);
@@ -3153,7 +3214,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (!files) files = await this.scanInternalFiles(); if (!files) files = await this.scanInternalFiles();
const filesOnDB = ((await this.localDatabase.localDatabase.allDocs({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted); const filesOnDB = ((await this.localDatabase.localDatabase.allDocs({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted);
const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => normalizePath(id2path(id2filenameInternalChunk(e._id))))])]; const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => normalizePath(id2path(id2filenameInternalMetadata(e._id))))])];
const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)) const allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1))
function compareMTime(a: number, b: number) { function compareMTime(a: number, b: number) {
const wa = ~~(a / 1000); const wa = ~~(a / 1000);
@@ -3198,7 +3259,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (ignorePatterns.some(e => filename.match(e))) continue; if (ignorePatterns.some(e => filename.match(e))) continue;
const fileOnStorage = files.find(e => e.path == filename); const fileOnStorage = files.find(e => e.path == filename);
const fileOnDatabase = filesOnDB.find(e => e._id == filename2idInternalChunk(id2path(filename))); const fileOnDatabase = filesOnDB.find(e => e._id == filename2idInternalMetadata(id2path(filename)));
const addProc = async (p: () => Promise<void>): Promise<void> => { const addProc = async (p: () => Promise<void>): Promise<void> => {
const releaser = await semaphore.acquire(1); const releaser = await semaphore.acquire(1);
try { try {
@@ -3234,7 +3295,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} else if (!fileOnStorage && fileOnDatabase) { } else if (!fileOnStorage && fileOnDatabase) {
if (direction == "push") { if (direction == "push") {
if (fileOnDatabase.deleted) return; if (fileOnDatabase.deleted) return;
await this.deleteInternalFileOnDatabase(filename); await this.deleteInternalFileOnDatabase(filename, false);
} else if (direction == "pull") { } else if (direction == "pull") {
if (await this.extractInternalFileFromDatabase(filename)) { if (await this.extractInternalFileFromDatabase(filename)) {
countUpdatedFolder(filename); countUpdatedFolder(filename);