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", "id": "obsidian-livesync",
"name": "Self-hosted LiveSync", "name": "Self-hosted LiveSync",
"version": "0.8.5", "version": "0.9.0",
"minAppVersion": "0.9.12", "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.", "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", "author": "vorotamoroz",

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "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.", "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", "main": "main.js",
"type": "module", "type": "module",

View File

@@ -28,6 +28,8 @@ import { path2id } from "./utils";
import { Logger } from "./lib/src/logger"; import { Logger } from "./lib/src/logger";
import { checkRemoteVersion, connectRemoteCouchDB, getLastPostFailedBySize } from "./utils_couchdb"; import { checkRemoteVersion, connectRemoteCouchDB, getLastPostFailedBySize } from "./utils_couchdb";
type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>;
export class LocalPouchDB { export class LocalPouchDB {
auth: Credential; auth: Credential;
dbname: string; dbname: string;
@@ -52,7 +54,7 @@ export class LocalPouchDB {
remoteLockedAndDeviceNotAccepted = false; remoteLockedAndDeviceNotAccepted = false;
changeHandler: PouchDB.Core.Changes<EntryDoc> = null; 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)[] } = {}; leafArrivedCallbacks: { [key: string]: (() => void)[] } = {};
@@ -61,6 +63,8 @@ export class LocalPouchDB {
docSent = 0; docSent = 0;
docSeq = ""; docSeq = "";
isMobile = false;
cancelHandler<T extends PouchDB.Core.Changes<EntryDoc> | PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>>(handler: T): T { cancelHandler<T extends PouchDB.Core.Changes<EntryDoc> | PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>>(handler: T): T {
if (handler != null) { if (handler != null) {
handler.removeAllListeners(); handler.removeAllListeners();
@@ -77,7 +81,7 @@ export class LocalPouchDB {
this.localDatabase.removeAllListeners(); this.localDatabase.removeAllListeners();
} }
constructor(settings: RemoteDBSettings, dbname: string) { constructor(settings: RemoteDBSettings, dbname: string, isMobile: boolean) {
this.auth = { this.auth = {
username: "", username: "",
password: "", password: "",
@@ -85,6 +89,7 @@ export class LocalPouchDB {
this.dbname = dbname; this.dbname = dbname;
this.settings = settings; this.settings = settings;
this.cancelHandler = this.cancelHandler.bind(this); this.cancelHandler = this.cancelHandler.bind(this);
this.isMobile = isMobile;
// this.initializeDatabase(); // this.initializeDatabase();
} }
@@ -699,77 +704,22 @@ export class LocalPouchDB {
replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean) { replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean) {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
await this.waitForGCComplete(); await this.waitForGCComplete();
this.closeReplication(); this.openOneshotReplication(
Logger("send all data to server", LOG_LEVEL.NOTICE); setting,
let notice: WrappedNotice = null; showingNotice,
if (showingNotice) { async (e) => { },
notice = NewNotice("Initializing", 0); false,
} (e) => {
this.syncStatus = "STARTED"; if (e === true) res(e);
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();
rej(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) { if (!this.isReady) {
Logger("Database is not ready."); Logger("Database is not ready.");
return false; return false;
@@ -789,9 +739,10 @@ export class LocalPouchDB {
Logger("Another replication running."); Logger("Another replication running.");
return false; return false;
} }
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI);
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
if (typeof dbret === "string") { 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; return false;
} }
@@ -830,189 +781,256 @@ export class LocalPouchDB {
return { db: dbret.db, info: dbret.info, syncOptionBase, syncOption }; 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> { openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>) {
return await runWithLock("replicate", false, () => { if (keepAlive) {
return this._openReplication(setting, keepAlive, showResult, callback, false); 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; async openOneshotReplication(
// last_seq: number = 200; setting: RemoteDBSettings,
async _openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>, retrying: boolean): Promise<boolean> { showResult: boolean,
const ret = await this.checkReplicationConnectivity(setting, keepAlive, retrying); callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>,
if (ret === false) return false; 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; let notice: WrappedNotice = null;
if (ret === false) {
Logger("Could not connect to server.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
return;
}
if (showResult) { if (showResult) {
notice = NewNotice("Looking for the point last synchronized point.", 0); notice = NewNotice("Looking for the point last synchronized point.", 0);
} }
const { db, syncOptionBase, syncOption } = ret; const { db, syncOptionBase } = ret;
//replicate once
this.syncStatus = "STARTED"; this.syncStatus = "STARTED";
this.updateInfo(); this.updateInfo();
let resolved = false;
const docArrivedOnStart = this.docArrived; const docArrivedOnStart = this.docArrived;
const docSentOnStart = this.docSent; const docSentOnStart = this.docSent;
if (!retrying) {
const _openReplicationSync = () => { // If initial replication, save setting to rollback
Logger("Sync Main Started"); this.originalSetting = setting;
if (!retrying) { }
this.originalSetting = setting; this.syncHandler = this.cancelHandler(this.syncHandler);
} if (!pushOnly && !pullOnly) {
this.syncHandler = this.cancelHandler(this.syncHandler); this.syncHandler = this.localDatabase.sync(db, { checkpoint: "target", ...syncOptionBase });
this.syncHandler = this.localDatabase.sync<EntryDoc>(db, {
...syncOption,
pull: {
checkpoint: "target",
},
push: {
checkpoint: "source",
},
});
this.syncHandler this.syncHandler
.on("active", () => {
this.syncStatus = "CONNECTED";
this.updateInfo();
Logger("Replication activated");
if (notice != null) notice.setMessage(`Activated..`);
})
.on("change", async (e) => { .on("change", async (e) => {
try { await this.replicationChangeDetected(e, notice, docSentOnStart, docArrivedOnStart, callback);
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
if (retrying) { if (retrying) {
if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) { if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) {
// restore sync values // restore configration.
Logger("Back into original settings once."); Logger("Back into original settings once.");
if (notice != null) notice.hide(); if (notice != null) notice.hide();
this.syncHandler = this.cancelHandler(this.syncHandler); 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) => { .on("complete", (e) => {
this.syncStatus = "COMPLETED"; this.replicationCompleted(notice, showResult);
this.updateInfo(); if (thisCallback != null) {
Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO); thisCallback(true);
if (notice != null) notice.hide(); }
if (!keepAlive) { });
this.syncHandler = this.cancelHandler(this.syncHandler); } else if (pullOnly) {
// if keep alive runnning, resolve here, 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) => { .on("complete", (e) => {
this.syncStatus = "ERRORED"; this.replicationCompleted(notice, showResult);
this.updateInfo(); if (thisCallback != null) {
this.syncHandler = this.cancelHandler(this.syncHandler); thisCallback(true);
if (notice != null) notice.hide(); }
Logger("Replication denied", LOG_LEVEL.NOTICE); });
} 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); Logger(e);
}) }
.on("error", (e) => { if (thisCallback != null) {
this.syncStatus = "ERRORED"; thisCallback(e);
this.syncHandler = this.cancelHandler(this.syncHandler); }
this.updateInfo(); })
if (notice != null) notice.hide(); .on("paused", (e) => this.replicationPaused(notice));
if (getLastPostFailedBySize()) { }
if (keepAlive) {
Logger("Replication stopped.", LOG_LEVEL.NOTICE); openContinuousReplication(setting: RemoteDBSettings, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>, retrying: boolean) {
} else { if (this.syncHandler != null) {
// Duplicate settings for smaller batch. Logger("Replication is already in progress.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
const xsetting: RemoteDBSettings = JSON.parse(JSON.stringify(setting)); return;
xsetting.batch_size = Math.ceil(xsetting.batch_size / 2); }
xsetting.batches_limit = Math.ceil(xsetting.batches_limit / 2); Logger("Before LiveSync, start OneShot once...");
if (xsetting.batch_size <= 3 || xsetting.batches_limit <= 3) { this.openOneshotReplication(
Logger("We can't replicate more lower value.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO); setting,
} else { showResult,
Logger(`Retry with lower batch size:${xsetting.batch_size}/${xsetting.batches_limit}`, showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO); callback,
this._openReplication(xsetting, keepAlive, showResult, callback, true); 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); .on("complete", (e) => this.replicationCompleted(notice, showResult))
Logger(e); .on("denied", (e) => this.replicationDeniend(notice, e))
} .on("error", (e) => {
}) this.replicationErrored(notice, e);
.on("paused", (e) => { Logger("Replication stopped.", LOG_LEVEL.NOTICE);
this.syncStatus = "PAUSED"; })
this.updateInfo(); .on("paused", (e) => this.replicationPaused(notice));
if (notice != null) notice.hide(); },
Logger("replication paused", LOG_LEVEL.VERBOSE); false,
if (keepAlive && !resolved) { true
// 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;
}
} }
originalSetting: RemoteDBSettings = null;
closeReplication() { closeReplication() {
this.syncStatus = "CLOSED"; this.syncStatus = "CLOSED";
this.updateInfo(); this.updateInfo();
@@ -1039,7 +1057,7 @@ export class LocalPouchDB {
username: setting.couchDB_USER, username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD, 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; if (typeof con == "string") return;
try { try {
await con.db.destroy(); await con.db.destroy();
@@ -1057,7 +1075,7 @@ export class LocalPouchDB {
username: setting.couchDB_USER, username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD, 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; if (typeof con2 === "string") return;
Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE); Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
} }
@@ -1067,7 +1085,7 @@ export class LocalPouchDB {
username: setting.couchDB_USER, username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD, 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") { if (typeof dbret === "string") {
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
return; return;
@@ -1101,7 +1119,7 @@ export class LocalPouchDB {
username: setting.couchDB_USER, username: setting.couchDB_USER,
password: setting.couchDB_PASSWORD, 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") { if (typeof dbret === "string") {
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
return; 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 { EntryDoc, LOG_LEVEL } from "./lib/src/types";
import { path2id, id2path } from "./utils"; import { path2id, id2path } from "./utils";
import { NewNotice, runWithLock } from "./lib/src/utils"; import { NewNotice, runWithLock } from "./lib/src/utils";
@@ -171,12 +171,18 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
), ),
new Setting(containerRemoteDatabaseEl).setName("Use the old connecting method").addToggle((toggle) =>
toggle.setValue(this.plugin.settings.disableRequestURI).onChange(async (value) => { new Setting(containerRemoteDatabaseEl)
this.plugin.settings.disableRequestURI = value; .setDesc("This feature is locked in mobile")
await this.plugin.saveSettings(); .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) 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); addScreenElement("0", containerRemoteDatabaseEl);
const containerLocalDatabaseEl = containerEl.createDiv(); const containerLocalDatabaseEl = containerEl.createDiv();
containerLocalDatabaseEl.createEl("h3", { text: "Local Database configuration" }); containerLocalDatabaseEl.createEl("h3", { text: "Local Database configuration" });

View File

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

View File

@@ -1,3 +1,4 @@
import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js"; import { PouchDB as PouchDB_ } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
console.dir(PouchDB)
export { PouchDB }; 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", adapter: "http",
auth, auth,
fetch: async function (url: string | Request, opts: RequestInit) { fetch: async function (url: string | Request, opts: RequestInit) {
let size_ok = true;
let size = ""; let size = "";
const localURL = url.toString().substring(uri.length); const localURL = url.toString().substring(uri.length);
const method = opts.method ?? "GET"; 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; const opts_length = opts.body.toString().length;
if (opts_length > 1024 * 1024 * 10) { if (opts_length > 1024 * 1024 * 10) {
// over 10MB // over 10MB
size_ok = false;
if (uri.contains(".cloudantnosqldb.")) { if (uri.contains(".cloudantnosqldb.")) {
last_post_successed = false; last_post_successed = false;
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE); 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) { } catch (ex) {
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE); 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; last_post_successed = false;
} }
Logger(ex); Logger(ex);
@@ -114,7 +113,8 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
return responce; return responce;
} catch (ex) { } catch (ex) {
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE); 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; last_post_successed = false;
} }
Logger(ex); Logger(ex);

View File

@@ -172,3 +172,19 @@ div.sls-setting-menu-btn {
background-color: var(--text-muted); background-color: var(--text-muted);
text-decoration: line-through; 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, "allowJs": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"types": ["svelte", "node"],
// "importsNotUsedAsValues": "error", // "importsNotUsedAsValues": "error",
"importHelpers": true, "importHelpers": true,
"alwaysStrict": true, "alwaysStrict": true,