Compare commits

...

4 Commits
0.8.5 ... 0.9.0

Author SHA1 Message Date
vorotamoroz
f613f1b887 New feature:
- Add database configuration check & fixing tool
2022-05-10 13:43:50 +09:00
vorotamoroz
88ef7c316a Fixed:
- Do not show error message when synchronization run automatically .
2022-05-09 11:08:10 +09:00
vorotamoroz
3fbecdf567 Fixed:
- Newly created files could not be synchronized.
2022-05-08 00:02:34 +09:00
vorotamoroz
5db3a374a9 Fixed:
- Freezing LiveSync on mobile devices.
2022-05-06 18:14:45 +09:00
10 changed files with 479 additions and 260 deletions

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.8.5",
"version": "0.9.0",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.8.5",
"version": "0.9.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.8.5",
"version": "0.9.0",
"license": "MIT",
"dependencies": {
"diff-match-patch": "^1.0.5",

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.8.5",
"version": "0.9.0",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",

View File

@@ -28,6 +28,8 @@ import { path2id } from "./utils";
import { Logger } from "./lib/src/logger";
import { checkRemoteVersion, connectRemoteCouchDB, getLastPostFailedBySize } from "./utils_couchdb";
type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>;
export class LocalPouchDB {
auth: Credential;
dbname: string;
@@ -52,7 +54,7 @@ export class LocalPouchDB {
remoteLockedAndDeviceNotAccepted = false;
changeHandler: PouchDB.Core.Changes<EntryDoc> = null;
syncHandler: PouchDB.Replication.Sync<EntryDoc> = null;
syncHandler: PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc> = null;
leafArrivedCallbacks: { [key: string]: (() => void)[] } = {};
@@ -61,6 +63,8 @@ export class LocalPouchDB {
docSent = 0;
docSeq = "";
isMobile = false;
cancelHandler<T extends PouchDB.Core.Changes<EntryDoc> | PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>>(handler: T): T {
if (handler != null) {
handler.removeAllListeners();
@@ -77,7 +81,7 @@ export class LocalPouchDB {
this.localDatabase.removeAllListeners();
}
constructor(settings: RemoteDBSettings, dbname: string) {
constructor(settings: RemoteDBSettings, dbname: string, isMobile: boolean) {
this.auth = {
username: "",
password: "",
@@ -85,6 +89,7 @@ export class LocalPouchDB {
this.dbname = dbname;
this.settings = settings;
this.cancelHandler = this.cancelHandler.bind(this);
this.isMobile = isMobile;
// this.initializeDatabase();
}
@@ -699,77 +704,22 @@ export class LocalPouchDB {
replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean) {
return new Promise(async (res, rej) => {
await this.waitForGCComplete();
this.closeReplication();
Logger("send all data to server", LOG_LEVEL.NOTICE);
let notice: WrappedNotice = null;
if (showingNotice) {
notice = NewNotice("Initializing", 0);
}
this.syncStatus = "STARTED";
this.updateInfo();
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
const auth: Credential = {
username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD,
};
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI);
if (typeof dbret === "string") {
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
if (notice != null) notice.hide();
return rej(`could not connect to ${uri}:${dbret}`);
}
const syncOptionBase: PouchDB.Replication.SyncOptions = {
pull: {
checkpoint: "target",
},
push: {
checkpoint: "source",
},
batches_limit: setting.batches_limit,
batch_size: setting.batch_size,
};
const db = dbret.db;
const totalCount = (await this.localDatabase.info()).doc_count;
//replicate once
const replicate = this.localDatabase.replicate.to(db, { checkpoint: "source", ...syncOptionBase });
replicate
.on("active", () => {
this.syncStatus = "CONNECTED";
this.updateInfo();
if (notice) {
notice.setMessage("CONNECTED");
}
})
.on("change", (e) => {
// no op.
this.docSent += e.docs.length;
this.updateInfo();
notice.setMessage(`SENDING:${e.docs_written}/${totalCount}`);
Logger(`replicateAllToServer: sending..:${e.docs.length}`);
})
.on("complete", (info) => {
this.syncStatus = "COMPLETED";
this.updateInfo();
Logger("replicateAllToServer: Completed", LOG_LEVEL.NOTICE);
this.cancelHandler(replicate);
if (notice != null) notice.hide();
res(true);
})
.on("error", (e) => {
this.syncStatus = "ERRORED";
this.updateInfo();
Logger("replicateAllToServer: Pulling Replication error", LOG_LEVEL.INFO);
Logger(e);
this.cancelHandler(replicate);
if (notice != null) notice.hide();
this.openOneshotReplication(
setting,
showingNotice,
async (e) => { },
false,
(e) => {
if (e === true) res(e);
rej(e);
});
},
true,
false
);
});
}
async checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean) {
async checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean, showResult: boolean) {
if (!this.isReady) {
Logger("Database is not ready.");
return false;
@@ -789,9 +739,10 @@ export class LocalPouchDB {
Logger("Another replication running.");
return false;
}
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI);
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
if (typeof dbret === "string") {
Logger(`could not connect to ${uri}: ${dbret}`, LOG_LEVEL.NOTICE);
Logger(`could not connect to ${uri}: ${dbret}`, showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
return false;
}
@@ -830,189 +781,256 @@ export class LocalPouchDB {
return { db: dbret.db, info: dbret.info, syncOptionBase, syncOption };
}
async openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>): Promise<boolean> {
return await runWithLock("replicate", false, () => {
return this._openReplication(setting, keepAlive, showResult, callback, false);
});
openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>) {
if (keepAlive) {
this.openContinuousReplication(setting, showResult, callback, false);
} else {
this.openOneshotReplication(setting, showResult, callback, false, null, false, false);
}
}
replicationActivated(notice: WrappedNotice) {
this.syncStatus = "CONNECTED";
this.updateInfo();
Logger("Replication activated");
if (notice != null) notice.setMessage(`Activated..`);
}
async replicationChangeDetected(e: PouchDB.Replication.SyncResult<EntryDoc>, notice: WrappedNotice, docSentOnStart: number, docArrivedOnStart: number, callback: ReplicationCallback) {
try {
if (e.direction == "pull") {
await callback(e.change.docs);
Logger(`replicated ${e.change.docs_read} doc(s)`);
this.docArrived += e.change.docs.length;
} else {
this.docSent += e.change.docs.length;
}
if (notice != null) {
notice.setMessage(`${this.docSent - docSentOnStart}${this.docArrived - docArrivedOnStart}`);
}
this.updateInfo();
} catch (ex) {
Logger("Replication callback error", LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.NOTICE);
//
}
}
replicationCompleted(notice: WrappedNotice, showResult: boolean) {
this.syncStatus = "COMPLETED";
this.updateInfo();
Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
if (notice != null) notice.hide();
this.syncHandler = this.cancelHandler(this.syncHandler);
}
replicationDeniend(notice: WrappedNotice, e: any) {
this.syncStatus = "ERRORED";
this.updateInfo();
this.syncHandler = this.cancelHandler(this.syncHandler);
if (notice != null) notice.hide();
Logger("Replication denied", LOG_LEVEL.NOTICE);
Logger(e);
}
replicationErrored(notice: WrappedNotice, e: any) {
this.syncStatus = "ERRORED";
this.syncHandler = this.cancelHandler(this.syncHandler);
this.updateInfo();
}
replicationPaused(notice: WrappedNotice) {
this.syncStatus = "PAUSED";
this.updateInfo();
if (notice != null) notice.hide();
Logger("replication paused", LOG_LEVEL.VERBOSE);
}
originalSetting: RemoteDBSettings = null;
// last_seq: number = 200;
async _openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>, retrying: boolean): Promise<boolean> {
const ret = await this.checkReplicationConnectivity(setting, keepAlive, retrying);
if (ret === false) return false;
async openOneshotReplication(
setting: RemoteDBSettings,
showResult: boolean,
callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>,
retrying: boolean,
callbackDone: (e: boolean | any) => void,
pushOnly: boolean,
pullOnly: boolean
): Promise<boolean> {
if (this.syncHandler != null) {
Logger("Replication is already in progress.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
return;
}
Logger("Oneshot Sync begin...");
let thisCallback = callbackDone;
const ret = await this.checkReplicationConnectivity(setting, true, retrying, showResult);
let notice: WrappedNotice = null;
if (ret === false) {
Logger("Could not connect to server.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
return;
}
if (showResult) {
notice = NewNotice("Looking for the point last synchronized point.", 0);
}
const { db, syncOptionBase, syncOption } = ret;
//replicate once
const { db, syncOptionBase } = ret;
this.syncStatus = "STARTED";
this.updateInfo();
let resolved = false;
const docArrivedOnStart = this.docArrived;
const docSentOnStart = this.docSent;
const _openReplicationSync = () => {
Logger("Sync Main Started");
if (!retrying) {
this.originalSetting = setting;
}
this.syncHandler = this.cancelHandler(this.syncHandler);
this.syncHandler = this.localDatabase.sync<EntryDoc>(db, {
...syncOption,
pull: {
checkpoint: "target",
},
push: {
checkpoint: "source",
},
});
if (!retrying) {
// If initial replication, save setting to rollback
this.originalSetting = setting;
}
this.syncHandler = this.cancelHandler(this.syncHandler);
if (!pushOnly && !pullOnly) {
this.syncHandler = this.localDatabase.sync(db, { checkpoint: "target", ...syncOptionBase });
this.syncHandler
.on("active", () => {
this.syncStatus = "CONNECTED";
this.updateInfo();
Logger("Replication activated");
if (notice != null) notice.setMessage(`Activated..`);
})
.on("change", async (e) => {
try {
if (e.direction == "pull") {
await callback(e.change.docs);
Logger(`replicated ${e.change.docs_read} doc(s)`);
this.docArrived += e.change.docs.length;
} else {
this.docSent += e.change.docs.length;
}
if (notice != null) {
notice.setMessage(`${this.docSent - docSentOnStart}${this.docArrived - docArrivedOnStart}`);
}
this.updateInfo();
} catch (ex) {
Logger("Replication callback error", LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.NOTICE);
}
// re-connect to retry with original setting
await this.replicationChangeDetected(e, notice, docSentOnStart, docArrivedOnStart, callback);
if (retrying) {
if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) {
// restore sync values
// restore configration.
Logger("Back into original settings once.");
if (notice != null) notice.hide();
this.syncHandler = this.cancelHandler(this.syncHandler);
this._openReplication(this.originalSetting, keepAlive, showResult, callback, false);
this.openOneshotReplication(this.originalSetting, showResult, callback, false, callbackDone, pushOnly, pullOnly);
}
}
})
.on("complete", (e) => {
this.syncStatus = "COMPLETED";
this.updateInfo();
Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
if (notice != null) notice.hide();
if (!keepAlive) {
this.syncHandler = this.cancelHandler(this.syncHandler);
// if keep alive runnning, resolve here,
this.replicationCompleted(notice, showResult);
if (thisCallback != null) {
thisCallback(true);
}
});
} else if (pullOnly) {
this.syncHandler = this.localDatabase.replicate.to(db, { checkpoint: "target", ...syncOptionBase });
this.syncHandler
.on("change", async (e) => {
await this.replicationChangeDetected({ direction: "pull", change: e }, notice, docSentOnStart, docArrivedOnStart, callback);
if (retrying) {
if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) {
// restore configration.
Logger("Back into original settings once.");
if (notice != null) notice.hide();
this.syncHandler = this.cancelHandler(this.syncHandler);
this.openOneshotReplication(this.originalSetting, showResult, callback, false, callbackDone, pushOnly, pullOnly);
}
}
})
.on("denied", (e) => {
this.syncStatus = "ERRORED";
this.updateInfo();
this.syncHandler = this.cancelHandler(this.syncHandler);
if (notice != null) notice.hide();
Logger("Replication denied", LOG_LEVEL.NOTICE);
.on("complete", (e) => {
this.replicationCompleted(notice, showResult);
if (thisCallback != null) {
thisCallback(true);
}
});
} else if (pushOnly) {
this.syncHandler = this.localDatabase.replicate.to(db, { checkpoint: "target", ...syncOptionBase });
this.syncHandler.on("complete", (e) => {
this.replicationCompleted(notice, showResult);
if (thisCallback != null) {
thisCallback(true);
}
});
}
this.syncHandler
.on("active", () => this.replicationActivated(notice))
.on("denied", (e) => {
this.replicationDeniend(notice, e);
if (thisCallback != null) {
thisCallback(e);
}
})
.on("error", (e) => {
this.replicationErrored(notice, e);
Logger("Replication stopped.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
if (notice != null) notice.hide();
if (getLastPostFailedBySize()) {
// Duplicate settings for smaller batch.
const xsetting: RemoteDBSettings = JSON.parse(JSON.stringify(setting));
xsetting.batch_size = Math.ceil(xsetting.batch_size / 2) + 2;
xsetting.batches_limit = Math.ceil(xsetting.batches_limit / 2) + 2;
if (xsetting.batch_size <= 5 && xsetting.batches_limit <= 5) {
Logger("We can't replicate more lower value.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
} else {
Logger(`Retry with lower batch size:${xsetting.batch_size}/${xsetting.batches_limit}`, showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
thisCallback = null;
this.openOneshotReplication(xsetting, showResult, callback, true, callbackDone, pushOnly, pullOnly);
}
} else {
Logger("Replication error", LOG_LEVEL.NOTICE);
Logger(e);
})
.on("error", (e) => {
this.syncStatus = "ERRORED";
this.syncHandler = this.cancelHandler(this.syncHandler);
this.updateInfo();
if (notice != null) notice.hide();
if (getLastPostFailedBySize()) {
if (keepAlive) {
Logger("Replication stopped.", LOG_LEVEL.NOTICE);
} else {
// Duplicate settings for smaller batch.
const xsetting: RemoteDBSettings = JSON.parse(JSON.stringify(setting));
xsetting.batch_size = Math.ceil(xsetting.batch_size / 2);
xsetting.batches_limit = Math.ceil(xsetting.batches_limit / 2);
if (xsetting.batch_size <= 3 || xsetting.batches_limit <= 3) {
Logger("We can't replicate more lower value.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
} else {
Logger(`Retry with lower batch size:${xsetting.batch_size}/${xsetting.batches_limit}`, showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
this._openReplication(xsetting, keepAlive, showResult, callback, true);
}
if (thisCallback != null) {
thisCallback(e);
}
})
.on("paused", (e) => this.replicationPaused(notice));
}
openContinuousReplication(setting: RemoteDBSettings, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>, retrying: boolean) {
if (this.syncHandler != null) {
Logger("Replication is already in progress.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
return;
}
Logger("Before LiveSync, start OneShot once...");
this.openOneshotReplication(
setting,
showResult,
callback,
false,
async () => {
Logger("LiveSync begin...");
const ret = await this.checkReplicationConnectivity(setting, true, true, showResult);
let notice: WrappedNotice = null;
if (ret === false) {
Logger("Could not connect to server.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
return;
}
if (showResult) {
notice = NewNotice("Looking for the point last synchronized point.", 0);
}
const { db, syncOption } = ret;
this.syncStatus = "STARTED";
this.updateInfo();
const docArrivedOnStart = this.docArrived;
const docSentOnStart = this.docSent;
if (!retrying) {
//TODO if successfly saven, roll back org setting.
this.originalSetting = setting;
}
this.syncHandler = this.cancelHandler(this.syncHandler);
this.syncHandler = this.localDatabase.sync<EntryDoc>(db, {
...syncOption,
pull: {
checkpoint: "target",
},
push: {
checkpoint: "source",
},
});
this.syncHandler
.on("active", () => this.replicationActivated(notice))
.on("change", async (e) => {
await this.replicationChangeDetected(e, notice, docSentOnStart, docArrivedOnStart, callback);
if (retrying) {
if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) {
// restore sync values
Logger("Back into original settings once.");
if (notice != null) notice.hide();
this.syncHandler = this.cancelHandler(this.syncHandler);
this.openContinuousReplication(this.originalSetting, showResult, callback, false);
}
}
} else {
Logger("Replication error", LOG_LEVEL.NOTICE);
Logger(e);
}
})
.on("paused", (e) => {
this.syncStatus = "PAUSED";
this.updateInfo();
if (notice != null) notice.hide();
Logger("replication paused", LOG_LEVEL.VERBOSE);
if (keepAlive && !resolved) {
// if keep alive runnning, resolve here,
resolved = true;
}
// Logger(e);
});
return this.syncHandler;
};
if (!keepAlive) {
await _openReplicationSync();
return true;
}
this.syncHandler = this.cancelHandler(this.syncHandler);
Logger("Pull before replicate.");
Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE);
Logger(await db.info(), LOG_LEVEL.VERBOSE);
let replicate: PouchDB.Replication.Replication<EntryDoc>;
try {
replicate = this.localDatabase.replicate.from(db, { checkpoint: "target", ...syncOptionBase });
replicate
.on("active", () => {
this.syncStatus = "CONNECTED";
this.updateInfo();
Logger("Replication pull activated.");
})
.on("change", async (e) => {
// when in first run, replication will send us tombstone data
// and in normal cases, all leavs should sent before the entry that contains these item.
// so skip to completed all, we should treat all changes.
try {
await callback(e.docs);
this.docArrived += e.docs.length;
this.updateInfo();
Logger(`pulled ${e.docs.length} doc(s)`);
if (notice != null) {
notice.setMessage(`Replication pulled:${e.docs_read}`);
}
} catch (ex) {
Logger("Replication callback error", LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.NOTICE);
}
});
this.syncStatus = "COMPLETED";
this.updateInfo();
this.cancelHandler(replicate);
this.syncHandler = this.cancelHandler(this.syncHandler);
Logger("Replication pull completed.");
_openReplicationSync();
return true;
} catch (ex) {
this.syncStatus = "ERRORED";
this.updateInfo();
Logger("Pulling Replication error:", LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.NOTICE);
this.cancelHandler(replicate);
this.syncHandler = this.cancelHandler(this.syncHandler);
if (notice != null) notice.hide();
throw ex;
}
})
.on("complete", (e) => this.replicationCompleted(notice, showResult))
.on("denied", (e) => this.replicationDeniend(notice, e))
.on("error", (e) => {
this.replicationErrored(notice, e);
Logger("Replication stopped.", LOG_LEVEL.NOTICE);
})
.on("paused", (e) => this.replicationPaused(notice));
},
false,
true
);
}
originalSetting: RemoteDBSettings = null;
closeReplication() {
this.syncStatus = "CLOSED";
this.updateInfo();
@@ -1039,7 +1057,7 @@ export class LocalPouchDB {
username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD,
};
const con = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI);
const con = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
if (typeof con == "string") return;
try {
await con.db.destroy();
@@ -1057,7 +1075,7 @@ export class LocalPouchDB {
username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD,
};
const con2 = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI);
const con2 = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
if (typeof con2 === "string") return;
Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
}
@@ -1067,7 +1085,7 @@ export class LocalPouchDB {
username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD,
};
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI);
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
if (typeof dbret === "string") {
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
return;
@@ -1101,7 +1119,7 @@ export class LocalPouchDB {
username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD,
};
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI);
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
if (typeof dbret === "string") {
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
return;

View File

@@ -1,4 +1,4 @@
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom } from "obsidian";
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl } from "obsidian";
import { EntryDoc, LOG_LEVEL } from "./lib/src/types";
import { path2id, id2path } from "./utils";
import { NewNotice, runWithLock } from "./lib/src/utils";
@@ -171,12 +171,18 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
),
new Setting(containerRemoteDatabaseEl).setName("Use the old connecting method").addToggle((toggle) =>
toggle.setValue(this.plugin.settings.disableRequestURI).onChange(async (value) => {
this.plugin.settings.disableRequestURI = value;
await this.plugin.saveSettings();
new Setting(containerRemoteDatabaseEl)
.setDesc("This feature is locked in mobile")
.setName("Use the old connecting method")
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.disableRequestURI).onChange(async (value) => {
this.plugin.settings.disableRequestURI = value;
await this.plugin.saveSettings();
});
toggle.setDisabled(this.plugin.isMobile);
return toggle;
})
)
);
new Setting(containerRemoteDatabaseEl)
@@ -191,6 +197,174 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
})
);
new Setting(containerRemoteDatabaseEl)
.setName("Check database configuration")
// .setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.")
.addButton((button) =>
button
.setButtonText("Check")
.setDisabled(false)
.onClick(async () => {
const checkConfig = async () => {
try {
const requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, key?: string, body?: string) => {
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`));
const encoded = window.btoa(utf8str);
const authHeader = "Basic " + encoded;
// const origin = "capacitor://localhost";
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };
const uri = `${baseUri}/_node/_local/_config${key ? "/" + key : ""}`;
const requestParam: RequestUrlParam = {
url: uri,
method: body ? "PUT" : "GET",
headers: transformedHeaders,
contentType: "application/json",
body: body ? JSON.stringify(body) : undefined,
};
return await requestUrl(requestParam);
};
const r = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, window.origin);
Logger(JSON.stringify(r.json, null, 2));
const responseConfig = r.json;
const emptyDiv = createDiv();
emptyDiv.innerHTML = "<span></span>";
checkResultDiv.replaceChildren(...[emptyDiv]);
const addResult = (msg: string, classes?: string[]) => {
const tmpDiv = createDiv();
tmpDiv.addClass("ob-btn-config-fix");
if (classes) {
tmpDiv.addClasses(classes);
}
tmpDiv.innerHTML = `${msg}`;
checkResultDiv.appendChild(tmpDiv);
};
const addConfigFixButton = (title: string, key: string, value: string) => {
const tmpDiv = createDiv();
tmpDiv.addClass("ob-btn-config-fix");
tmpDiv.innerHTML = `<label>${title}</label><button>Fix</button>`;
const x = checkResultDiv.appendChild(tmpDiv);
x.querySelector("button").addEventListener("click", async () => {
console.dir({ key, value });
const res = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, undefined, key, value);
console.dir(res);
if (res.status == 200) {
Logger(`${title} successfly updated`, LOG_LEVEL.NOTICE);
checkResultDiv.removeChild(x);
checkConfig();
} else {
Logger(`${title} failed`, LOG_LEVEL.NOTICE);
Logger(res.text);
}
});
};
addResult("---Notice---", ["ob-btn-config-head"]);
addResult(
"If the server configuration is not persistent (e.g., running on docker), the values set from here will also be volatile. Once you are able to connect, please reflect the settings in the server's local.ini.",
["ob-btn-config-info"]
);
addResult("Your configuration is dumped to Log", ["ob-btn-config-info"]);
addResult("--Config check--", ["ob-btn-config-head"]);
// Admin check
// for database creation and deletion
if (!(this.plugin.settings.couchDB_USER in responseConfig.admins)) {
addResult(`⚠ You do not have administrative privileges.`);
} else {
addResult("✔ You have administrative privileges.");
}
// HTTP user-authorization check
if (responseConfig?.chttpd?.require_valid_user != "true") {
addResult("❗ chttpd.require_valid_user looks like wrong.");
addConfigFixButton("Set chttpd.require_valid_user = true", "chttpd/require_valid_user", "true");
} else {
addResult("✔ chttpd.require_valid_user is ok.");
}
if (responseConfig?.chttpd_auth?.require_valid_user != "true") {
addResult("❗ chttpd_auth.require_valid_user looks like wrong.");
addConfigFixButton("Set chttpd_auth.require_valid_user = true", "chttpd_auth/require_valid_user", "true");
} else {
addResult("✔ chttpd_auth.require_valid_user is ok.");
}
// HTTPD check
// Check Authentication header
if (!responseConfig?.httpd["WWW-Authenticate"]) {
addResult("❗ httpd.WWW-Authenticate is missing");
addConfigFixButton("Set httpd.WWW-Authenticate", "httpd/WWW-Authenticate", 'Basic realm="couchdb"');
} else {
addResult("✔ httpd.WWW-Authenticate is ok.");
}
if (responseConfig?.httpd?.enable_cors != "true") {
addResult("❗ httpd.enable_cors is wrong");
addConfigFixButton("Set httpd.enable_cors", "httpd/enable_cors", "true");
} else {
addResult("✔ httpd.enable_cors is ok.");
}
// CORS check
// checking connectivity for mobile
if (responseConfig?.cors?.credentials != "true") {
addResult("❗ cors.credentials is wrong");
addConfigFixButton("Set cors.credentials", "cors/credentials", "true");
} else {
addResult("✔ cors.credentials is ok.");
}
const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(",");
if (
responseConfig?.cors?.origins == "*" ||
(ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 && ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && ConfiguredOrigins.indexOf("http://localhost") !== -1)
) {
addResult("✔ cors.origins is ok.");
} else {
addResult("❗ cors.origins is wrong");
addConfigFixButton("Set cors.origins", "cors/origins", "app://obsidian.md,capacitor://localhost,http://localhost");
}
addResult("--Connection check--", ["ob-btn-config-head"]);
addResult(`Current origin:${window.location.origin}`);
// Request header check
const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"];
for (const org of origins) {
const rr = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, org);
const responseHeaders = Object.entries(rr.headers)
.map((e) => {
e[0] = (e[0] + "").toLowerCase();
return e;
})
.reduce((obj, [key, val]) => {
obj[key] = val;
return obj;
}, {});
addResult(`Origin check:${org}`);
if (responseHeaders["access-control-allow-credentials"] != "true") {
addResult("❗ CORS is not allowing credential");
} else {
addResult("✔ CORS credential OK");
}
if (responseHeaders["access-control-allow-origin"] != org) {
addResult(`❗ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}`);
} else {
addResult("✔ CORS origin OK");
}
}
addResult("--Done--", ["ob-btn-config-haed"]);
addResult("If you have some trouble with Connection-check even though all Config-check has been passed, Please check your reverse proxy's configuration.", ["ob-btn-config-info"]);
} catch (ex) {
Logger(`Checking configration failed`);
Logger(ex);
}
};
await checkConfig();
})
);
const checkResultDiv = containerRemoteDatabaseEl.createEl("div", {
text: "",
});
addScreenElement("0", containerRemoteDatabaseEl);
const containerLocalDatabaseEl = containerEl.createDiv();
containerLocalDatabaseEl.createEl("h3", { text: "Local Database configuration" });

View File

@@ -64,6 +64,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
statusBar2: HTMLElement;
suspended: boolean;
deviceAndVaultName: string;
isMobile = false;
setInterval(handler: () => any, timeout?: number): number {
const timer = window.setInterval(handler, timeout);
@@ -93,6 +94,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName();
const last_version = localStorage.getItem(lsname);
await this.loadSettings();
//@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;
@@ -180,7 +186,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.realizeSettingSyncMode();
this.registerWatchEvents();
if (this.settings.syncOnStart) {
await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
}
} catch (ex) {
Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE);
@@ -190,8 +196,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.addCommand({
id: "livesync-replicate",
name: "Replicate now",
callback: () => {
this.replicate();
callback: async () => {
await this.replicate();
},
});
this.addCommand({
@@ -306,7 +312,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
const vaultName = this.app.vault.getName();
Logger("Open Database...");
this.localDatabase = new LocalPouchDB(this.settings, vaultName);
//@ts-ignore
const isMobile = this.app.isMobile;
this.localDatabase = new LocalPouchDB(this.settings, vaultName, isMobile);
this.localDatabase.updateInfo = () => {
this.refreshStatusText();
};
@@ -365,7 +373,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
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.watchVaultChange));
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
}
@@ -389,10 +397,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.sweepPlugin(false);
}
if (this.settings.liveSync) {
await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
}
if (this.settings.syncOnStart) {
await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
}
if (this.settings.periodicReplication) {
this.setPeriodicSync();
@@ -408,7 +416,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async watchWorkspaceOpenAsync(file: TFile) {
await this.applyBatchChange();
if (file == null) return;
if (file == null) {
return;
}
if (this.settings.syncOnFileOpen && !this.suspended) {
await this.replicate();
}
@@ -449,7 +459,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async applyBatchChange() {
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
return [];
return;
}
return await runWithLock("batchSave", false, async () => {
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
@@ -467,7 +477,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
});
this.refreshStatusText();
return await Promise.all(promises);
await allSettledWithConcurrencyLimit(promises, 3);
return;
});
}
@@ -902,7 +913,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.sweepPlugin(false);
}
if (this.settings.liveSync) {
await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
this.refreshStatusText();
}
this.setPeriodicSync();
@@ -971,7 +982,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.settings.autoSweepPlugins) {
await this.sweepPlugin(false);
}
await this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
}
async initializeDatabase(showingNotice?: boolean) {

View File

@@ -1,3 +1,4 @@
import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
console.dir(PouchDB)
export { PouchDB };
import { PouchDB as PouchDB_ } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
const Pouch: PouchDB.Static = PouchDB_;
export { Pouch as PouchDB };

View File

@@ -41,7 +41,6 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
adapter: "http",
auth,
fetch: async function (url: string | Request, opts: RequestInit) {
let size_ok = true;
let size = "";
const localURL = url.toString().substring(uri.length);
const method = opts.method ?? "GET";
@@ -49,7 +48,6 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
const opts_length = opts.body.toString().length;
if (opts_length > 1024 * 1024 * 10) {
// over 10MB
size_ok = false;
if (uri.contains(".cloudantnosqldb.")) {
last_post_successed = false;
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
@@ -93,7 +91,8 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
});
} catch (ex) {
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
if (!size_ok && (method == "POST" || method == "PUT")) {
// limit only in bulk_docs.
if (url.toString().indexOf("_bulk_docs") !== -1) {
last_post_successed = false;
}
Logger(ex);
@@ -114,7 +113,8 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
return responce;
} catch (ex) {
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
if (!size_ok && (method == "POST" || method == "PUT")) {
// limit only in bulk_docs.
if (url.toString().indexOf("_bulk_docs") !== -1) {
last_post_successed = false;
}
Logger(ex);

View File

@@ -172,3 +172,19 @@ div.sls-setting-menu-btn {
background-color: var(--text-muted);
text-decoration: line-through;
}
.ob-btn-config-fix label {
margin-right: 40px;
}
.ob-btn-config-info {
border: 1px solid salmon;
padding: 2px;
margin: 1px;
border-radius: 4px;
}
.ob-btn-config-head {
padding: 2px;
margin: 1px;
border-radius: 4px;
}

View File

@@ -6,7 +6,6 @@
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"types": ["svelte", "node"],
// "importsNotUsedAsValues": "error",
"importHelpers": true,
"alwaysStrict": true,