mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-23 04:28:48 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f613f1b887 | ||
|
|
88ef7c316a | ||
|
|
3fbecdf567 | ||
|
|
5db3a374a9 | ||
|
|
6f76f90075 | ||
|
|
9acf9fe093 | ||
|
|
1e3de47d92 | ||
|
|
a50f0965f6 | ||
|
|
9d3aa35b0b |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "src/lib"]
|
||||
path = src/lib
|
||||
url = https://github.com/vrtmrz/livesync-commonlib
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.8.2",
|
||||
"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",
|
||||
|
||||
37
package-lock.json
generated
37
package-lock.json
generated
@@ -1,15 +1,18 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"esbuild": "0.13.12",
|
||||
"esbuild-svelte": "^0.6.0",
|
||||
"svelte-preprocess": "^4.10.2",
|
||||
"xxhash-wasm": "^0.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -27,7 +30,7 @@
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"obsidian": "^0.13.30",
|
||||
"obsidian": "^0.14.6",
|
||||
"rollup": "^2.32.1",
|
||||
"svelte-preprocess": "^4.10.2",
|
||||
"tslib": "^2.2.0",
|
||||
@@ -2571,9 +2574,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"version": "2.29.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -2659,15 +2662,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "0.13.30",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.30.tgz",
|
||||
"integrity": "sha512-uAOrIyeHE9qYzg1Qjfpy/qlyLUFX9oyKWeHYO8NVDoI+pm5VUTMe7XWcsXPwb9iVsVmggVJcdV15Vqm9bljhxQ==",
|
||||
"version": "0.14.6",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.14.6.tgz",
|
||||
"integrity": "sha512-oXPJ8Zt10WhN19bk5l4mZuXRZbbdT1QoMgxGGJ0bB7UcJa0bozDzugS5L/QiV9gDoujpUPxDWNVahEel6r0Fpw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^0.19.6",
|
||||
"@codemirror/view": "^0.19.31",
|
||||
"@types/codemirror": "0.0.108",
|
||||
"moment": "2.29.1"
|
||||
"moment": "2.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
@@ -5406,9 +5409,9 @@
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"version": "2.29.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
@@ -5470,15 +5473,15 @@
|
||||
}
|
||||
},
|
||||
"obsidian": {
|
||||
"version": "0.13.30",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.30.tgz",
|
||||
"integrity": "sha512-uAOrIyeHE9qYzg1Qjfpy/qlyLUFX9oyKWeHYO8NVDoI+pm5VUTMe7XWcsXPwb9iVsVmggVJcdV15Vqm9bljhxQ==",
|
||||
"version": "0.14.6",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.14.6.tgz",
|
||||
"integrity": "sha512-oXPJ8Zt10WhN19bk5l4mZuXRZbbdT1QoMgxGGJ0bB7UcJa0bozDzugS5L/QiV9gDoujpUPxDWNVahEel6r0Fpw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@codemirror/state": "^0.19.6",
|
||||
"@codemirror/view": "^0.19.31",
|
||||
"@types/codemirror": "0.0.108",
|
||||
"moment": "2.29.1"
|
||||
"moment": "2.29.2"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
|
||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.8.2",
|
||||
"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",
|
||||
@@ -21,20 +21,23 @@
|
||||
"@types/pouchdb-browser": "^6.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"obsidian": "^0.13.30",
|
||||
"rollup": "^2.32.1",
|
||||
"tslib": "^2.2.0",
|
||||
"typescript": "^4.2.4",
|
||||
"builtin-modules": "^3.2.0",
|
||||
"esbuild": "0.13.12",
|
||||
"esbuild-svelte": "^0.6.0",
|
||||
"svelte-preprocess": "^4.10.2"
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"obsidian": "^0.14.6",
|
||||
"rollup": "^2.32.1",
|
||||
"svelte-preprocess": "^4.10.2",
|
||||
"tslib": "^2.2.0",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"esbuild": "0.13.12",
|
||||
"esbuild-svelte": "^0.6.0",
|
||||
"svelte-preprocess": "^4.10.2",
|
||||
"xxhash-wasm": "^0.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# PouchDB-browser
|
||||
just webpacked.
|
||||
|
||||
Just webpacked.
|
||||
(Rollup couldn't pack pouchdb-browser into browser bundle)
|
||||
|
||||
File diff suppressed because one or more lines are too long
9820
pouchdb-browser-webpack/package-lock.json
generated
9820
pouchdb-browser-webpack/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pouchdb-browser": "^7.2.2"
|
||||
"pouchdb-browser": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "^5.58.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||
import { diff_result } from "./types";
|
||||
import { escapeStringToHTML } from "./utils";
|
||||
import { diff_result } from "./lib/src/types";
|
||||
import { escapeStringToHTML } from "./lib/src/utils";
|
||||
|
||||
export class ConflictResolveModal extends Modal {
|
||||
// result: Array<[number, string]>;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { TFile, Modal, App } from "obsidian";
|
||||
import { path2id, escapeStringToHTML } from "./utils";
|
||||
import { path2id } from "./utils";
|
||||
import { escapeStringToHTML } from "./lib/src/utils";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||
import { LOG_LEVEL } from "./types";
|
||||
import { Logger } from "./logger";
|
||||
import { LOG_LEVEL } from "./lib/src/types";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
|
||||
export class DocumentHistoryModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Notice } from "obsidian";
|
||||
import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
||||
import { PouchDB } from "./pouchdb-browser";
|
||||
import xxhash from "xxhash-wasm";
|
||||
import {
|
||||
Entry,
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
NewEntry,
|
||||
PlainEntry,
|
||||
LoadedEntry,
|
||||
ObsidianLiveSyncSettings,
|
||||
Credential,
|
||||
EntryMilestoneInfo,
|
||||
LOG_LEVEL,
|
||||
@@ -22,16 +20,20 @@ import {
|
||||
VER,
|
||||
MILSTONE_DOCID,
|
||||
DatabaseConnectingStatus,
|
||||
} from "./types";
|
||||
import { resolveWithIgnoreKnownError, delay, path2id, runWithLock, isPlainText } from "./utils";
|
||||
import { Logger } from "./logger";
|
||||
} from "./lib/src/types";
|
||||
import { decrypt, encrypt } from "./lib/src/e2ee";
|
||||
import { RemoteDBSettings } from "./lib/src/types";
|
||||
import { resolveWithIgnoreKnownError, delay, runWithLock, isPlainText, splitPieces, NewNotice, WrappedNotice } from "./lib/src/utils";
|
||||
import { path2id } from "./utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { checkRemoteVersion, connectRemoteCouchDB, getLastPostFailedBySize } from "./utils_couchdb";
|
||||
import { decrypt, encrypt } from "./e2ee";
|
||||
|
||||
type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>;
|
||||
|
||||
export class LocalPouchDB {
|
||||
auth: Credential;
|
||||
dbname: string;
|
||||
settings: ObsidianLiveSyncSettings;
|
||||
settings: RemoteDBSettings;
|
||||
localDatabase: PouchDB.Database<EntryDoc>;
|
||||
nodeid = "";
|
||||
isReady = false;
|
||||
@@ -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: ObsidianLiveSyncSettings, 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();
|
||||
}
|
||||
@@ -503,7 +508,7 @@ export class LocalPouchDB {
|
||||
}
|
||||
async putDBEntry(note: LoadedEntry) {
|
||||
await this.waitForGCComplete();
|
||||
let leftData = note.data;
|
||||
// let leftData = note.data;
|
||||
const savenNotes = [];
|
||||
let processed = 0;
|
||||
let made = 0;
|
||||
@@ -516,53 +521,22 @@ export class LocalPouchDB {
|
||||
pieceSize = MAX_DOC_SIZE;
|
||||
plainSplit = true;
|
||||
}
|
||||
|
||||
const newLeafs: EntryLeaf[] = [];
|
||||
do {
|
||||
// To keep low bandwith and database size,
|
||||
// Dedup pieces on database.
|
||||
// from 0.1.10, for best performance. we use markdown delimiters
|
||||
// 1. \n[^\n]{longLineThreshold}[^\n]*\n -> long sentence shuld break.
|
||||
// 2. \n\n shold break
|
||||
// 3. \r\n\r\n should break
|
||||
// 4. \n# should break.
|
||||
let cPieceSize = pieceSize;
|
||||
if (plainSplit) {
|
||||
let minimumChunkSize = this.settings.minimumChunkSize;
|
||||
if (minimumChunkSize < 10) minimumChunkSize = 10;
|
||||
let longLineThreshold = this.settings.longLineThreshold;
|
||||
if (longLineThreshold < 100) longLineThreshold = 100;
|
||||
cPieceSize = 0;
|
||||
// lookup for next splittion .
|
||||
// we're standing on "\n"
|
||||
do {
|
||||
const n1 = leftData.indexOf("\n", cPieceSize + 1);
|
||||
const n2 = leftData.indexOf("\n\n", cPieceSize + 1);
|
||||
const n3 = leftData.indexOf("\r\n\r\n", cPieceSize + 1);
|
||||
const n4 = leftData.indexOf("\n#", cPieceSize + 1);
|
||||
if (n1 == -1 && n2 == -1 && n3 == -1 && n4 == -1) {
|
||||
cPieceSize = MAX_DOC_SIZE;
|
||||
break;
|
||||
}
|
||||
// To keep low bandwith and database size,
|
||||
// Dedup pieces on database.
|
||||
// from 0.1.10, for best performance. we use markdown delimiters
|
||||
// 1. \n[^\n]{longLineThreshold}[^\n]*\n -> long sentence shuld break.
|
||||
// 2. \n\n shold break
|
||||
// 3. \r\n\r\n should break
|
||||
// 4. \n# should break.
|
||||
let minimumChunkSize = this.settings.minimumChunkSize;
|
||||
if (minimumChunkSize < 10) minimumChunkSize = 10;
|
||||
let longLineThreshold = this.settings.longLineThreshold;
|
||||
if (longLineThreshold < 100) longLineThreshold = 100;
|
||||
|
||||
if (n1 > longLineThreshold) {
|
||||
// long sentence is an established piece
|
||||
cPieceSize = n1;
|
||||
} else {
|
||||
// cPieceSize = Math.min.apply([n2, n3, n4].filter((e) => e > 1));
|
||||
// ^ heavy.
|
||||
if (n1 > 0 && cPieceSize < n1) cPieceSize = n1;
|
||||
if (n2 > 0 && cPieceSize < n2) cPieceSize = n2 + 1;
|
||||
if (n3 > 0 && cPieceSize < n3) cPieceSize = n3 + 3;
|
||||
// Choose shorter, empty line and \n#
|
||||
if (n4 > 0 && cPieceSize > n4) cPieceSize = n4 + 0;
|
||||
cPieceSize++;
|
||||
}
|
||||
} while (cPieceSize < minimumChunkSize);
|
||||
}
|
||||
|
||||
// piece size determined.
|
||||
const piece = leftData.substring(0, cPieceSize);
|
||||
leftData = leftData.substring(cPieceSize);
|
||||
const pieces = splitPieces(note.data, pieceSize, plainSplit, minimumChunkSize, longLineThreshold);
|
||||
for (const piece of pieces()) {
|
||||
processed++;
|
||||
let leafid = "";
|
||||
// Get hash of piece.
|
||||
@@ -646,7 +620,7 @@ export class LocalPouchDB {
|
||||
}
|
||||
}
|
||||
savenNotes.push(leafid);
|
||||
} while (leftData != "");
|
||||
}
|
||||
let saved = true;
|
||||
if (newLeafs.length > 0) {
|
||||
try {
|
||||
@@ -727,80 +701,25 @@ export class LocalPouchDB {
|
||||
// no op now,
|
||||
return true;
|
||||
}
|
||||
replicateAllToServer(setting: ObsidianLiveSyncSettings, showingNotice?: boolean) {
|
||||
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: Notice = null;
|
||||
if (showingNotice) {
|
||||
notice = new Notice("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: ObsidianLiveSyncSettings, keepAlive: boolean, skipCheck: boolean) {
|
||||
async checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean, showResult: boolean) {
|
||||
if (!this.isReady) {
|
||||
Logger("Database is not ready.");
|
||||
return false;
|
||||
@@ -808,7 +727,7 @@ export class LocalPouchDB {
|
||||
|
||||
await this.waitForGCComplete();
|
||||
if (setting.versionUpFlash != "") {
|
||||
new Notice("Open settings and check message, please.");
|
||||
NewNotice("Open settings and check message, please.");
|
||||
return false;
|
||||
}
|
||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
||||
@@ -820,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;
|
||||
}
|
||||
|
||||
@@ -839,15 +759,6 @@ export class LocalPouchDB {
|
||||
locked: false,
|
||||
accepted_nodes: [this.nodeid],
|
||||
};
|
||||
// const remoteInfo = dbret.info;
|
||||
// const localInfo = await this.localDatabase.info();
|
||||
// const remoteDocsCount = remoteInfo.doc_count;
|
||||
// const localDocsCount = localInfo.doc_count;
|
||||
// const remoteUpdSeq = typeof remoteInfo.update_seq == "string" ? Number(remoteInfo.update_seq.split("-")[0]) : remoteInfo.update_seq;
|
||||
// const localUpdSeq = typeof localInfo.update_seq == "string" ? Number(localInfo.update_seq.split("-")[0]) : localInfo.update_seq;
|
||||
|
||||
// Logger(`Database diffences: remote:${remoteDocsCount} docs / last update ${remoteUpdSeq}`);
|
||||
// Logger(`Database diffences: local :${localDocsCount} docs / last update ${localUpdSeq}`);
|
||||
|
||||
const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint);
|
||||
this.remoteLocked = remoteMilestone.locked;
|
||||
@@ -870,191 +781,256 @@ export class LocalPouchDB {
|
||||
return { db: dbret.db, info: dbret.info, syncOptionBase, syncOption };
|
||||
}
|
||||
|
||||
async openReplication(setting: ObsidianLiveSyncSettings, 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: ObsidianLiveSyncSettings = null;
|
||||
// last_seq: number = 200;
|
||||
async _openReplication(setting: ObsidianLiveSyncSettings, 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;
|
||||
let notice: Notice = null;
|
||||
if (showResult) {
|
||||
notice = new Notice("Looking for the point last synchronized point.", 0);
|
||||
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;
|
||||
}
|
||||
const { db, syncOptionBase, syncOption } = ret;
|
||||
//replicate once
|
||||
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 } = 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") {
|
||||
// console.log(`pulled data:${e.change.docs.map((e) => e._id).join(",")}`);
|
||||
await callback(e.change.docs);
|
||||
Logger(`replicated ${e.change.docs_read} doc(s)`);
|
||||
this.docArrived += e.change.docs.length;
|
||||
} else {
|
||||
// console.log(`put data:${e.change.docs.map((e) => e._id).join(",")}`);
|
||||
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: ObsidianLiveSyncSettings = 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();
|
||||
@@ -1074,14 +1050,14 @@ export class LocalPouchDB {
|
||||
this.disposeHashCache();
|
||||
Logger("Local Database Reset", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
async tryResetRemoteDatabase(setting: ObsidianLiveSyncSettings) {
|
||||
async tryResetRemoteDatabase(setting: RemoteDBSettings) {
|
||||
await this.closeReplication();
|
||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
||||
const auth: Credential = {
|
||||
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();
|
||||
@@ -1092,24 +1068,24 @@ export class LocalPouchDB {
|
||||
Logger(ex, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
}
|
||||
async tryCreateRemoteDatabase(setting: ObsidianLiveSyncSettings) {
|
||||
async tryCreateRemoteDatabase(setting: RemoteDBSettings) {
|
||||
await this.closeReplication();
|
||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
||||
const auth: Credential = {
|
||||
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);
|
||||
}
|
||||
async markRemoteLocked(setting: ObsidianLiveSyncSettings, locked: boolean) {
|
||||
async markRemoteLocked(setting: RemoteDBSettings, locked: boolean) {
|
||||
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);
|
||||
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;
|
||||
@@ -1137,13 +1113,13 @@ export class LocalPouchDB {
|
||||
}
|
||||
await dbret.db.put(remoteMilestone);
|
||||
}
|
||||
async markRemoteResolved(setting: ObsidianLiveSyncSettings) {
|
||||
async markRemoteResolved(setting: RemoteDBSettings) {
|
||||
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);
|
||||
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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
import { escapeStringToHTML } from "./utils";
|
||||
import { escapeStringToHTML } from "./lib/src/utils";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
|
||||
export class LogDisplayModal extends Modal {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { App, Notice, PluginSettingTab, Setting, sanitizeHTMLToDom } from "obsidian";
|
||||
import { EntryDoc, LOG_LEVEL } from "./types";
|
||||
import { path2id, id2path, runWithLock } from "./utils";
|
||||
import { Logger } from "./logger";
|
||||
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";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { connectRemoteCouchDB } from "./utils_couchdb";
|
||||
import { testCrypt } from "./e2ee";
|
||||
import { testCrypt } from "./lib/src/e2ee";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
|
||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
@@ -170,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)
|
||||
@@ -190,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" });
|
||||
@@ -688,7 +863,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.onClick(async () => {
|
||||
const files = this.app.vault.getFiles();
|
||||
Logger("Verify and repair all files started", LOG_LEVEL.NOTICE);
|
||||
const notice = new Notice("", 0);
|
||||
const notice = NewNotice("", 0);
|
||||
let i = 0;
|
||||
for (const file of files) {
|
||||
i++;
|
||||
@@ -714,7 +889,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
const notice = new Notice("", 0);
|
||||
const notice = NewNotice("", 0);
|
||||
Logger(`Begin sanity check`, LOG_LEVEL.INFO);
|
||||
notice.setMessage(`Begin sanity check`);
|
||||
await runWithLock("sancheck", true, async () => {
|
||||
@@ -834,7 +1009,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
const updateDisabledOfDeviceAndVaultName = () => {
|
||||
vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic);
|
||||
vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto sweep." : "");
|
||||
vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto scan." : "");
|
||||
};
|
||||
new Setting(containerPluginSettings).setName("Enable plugin synchronization").addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
|
||||
@@ -844,8 +1019,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
);
|
||||
|
||||
new Setting(containerPluginSettings)
|
||||
.setName("Sweep plugins automatically")
|
||||
.setDesc("Sweep plugins before replicating.")
|
||||
.setName("Scan plugins automatically")
|
||||
.setDesc("Scan plugins before replicating.")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => {
|
||||
this.plugin.settings.autoSweepPlugins = value;
|
||||
@@ -855,8 +1030,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
);
|
||||
|
||||
new Setting(containerPluginSettings)
|
||||
.setName("Sweep plugins periodically")
|
||||
.setDesc("Sweep plugins each 1 minutes.")
|
||||
.setName("Scan plugins periodically")
|
||||
.setDesc("Scan plugins each 1 minutes.")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
||||
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { onMount } from "svelte";
|
||||
import { DevicePluginList, PluginDataEntry } from "./types";
|
||||
import { versionNumberString2Number } from "./utils";
|
||||
import { versionNumberString2Number } from "./lib/src/utils";
|
||||
|
||||
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
|
||||
<div class="ols-plugins-div-buttons">
|
||||
<button class="mod-cta" on:click={checkUpdates}>Check Updates</button>
|
||||
<button class="mod-cta" on:click={sweepPlugins}>Sweep installed</button>
|
||||
<button class="mod-cta" on:click={sweepPlugins}>Scan installed</button>
|
||||
<button class="mod-cta" on:click={applyPlugins}>Apply all</button>
|
||||
</div>
|
||||
<!-- <div class="ols-plugins-div-buttons">-->
|
||||
|
||||
168
src/e2ee.ts
168
src/e2ee.ts
@@ -1,168 +0,0 @@
|
||||
import { Logger } from "./logger";
|
||||
import { LOG_LEVEL } from "./types";
|
||||
|
||||
export type encodedData = [encryptedData: string, iv: string, salt: string];
|
||||
export type KeyBuffer = {
|
||||
index: string;
|
||||
key: CryptoKey;
|
||||
salt: Uint8Array;
|
||||
};
|
||||
|
||||
const KeyBuffs: KeyBuffer[] = [];
|
||||
const decKeyBuffs: KeyBuffer[] = [];
|
||||
|
||||
const KEY_RECYCLE_COUNT = 100;
|
||||
let recycleCount = KEY_RECYCLE_COUNT;
|
||||
|
||||
let semiStaticFieldBuffer: Uint8Array = null;
|
||||
const nonceBuffer: Uint32Array = new Uint32Array(1);
|
||||
|
||||
export async function getKeyForEncrypt(passphrase: string): Promise<[CryptoKey, Uint8Array]> {
|
||||
// For performance, the plugin reuses the key KEY_RECYCLE_COUNT times.
|
||||
const f = KeyBuffs.find((e) => e.index == passphrase);
|
||||
if (f) {
|
||||
recycleCount--;
|
||||
if (recycleCount > 0) {
|
||||
return [f.key, f.salt];
|
||||
}
|
||||
KeyBuffs.remove(f);
|
||||
recycleCount = KEY_RECYCLE_COUNT;
|
||||
}
|
||||
const xpassphrase = new TextEncoder().encode(passphrase);
|
||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase);
|
||||
const keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]);
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt,
|
||||
iterations: 100000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
KeyBuffs.push({
|
||||
index: passphrase,
|
||||
key,
|
||||
salt,
|
||||
});
|
||||
while (KeyBuffs.length > 50) {
|
||||
KeyBuffs.shift();
|
||||
}
|
||||
return [key, salt];
|
||||
}
|
||||
|
||||
export async function getKeyForDecryption(passphrase: string, salt: Uint8Array): Promise<[CryptoKey, Uint8Array]> {
|
||||
const bufKey = passphrase + uint8ArrayToHexString(salt);
|
||||
const f = decKeyBuffs.find((e) => e.index == bufKey);
|
||||
if (f) {
|
||||
return [f.key, f.salt];
|
||||
}
|
||||
const xpassphrase = new TextEncoder().encode(passphrase);
|
||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase);
|
||||
const keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]);
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt,
|
||||
iterations: 100000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["decrypt"]
|
||||
);
|
||||
decKeyBuffs.push({
|
||||
index: bufKey,
|
||||
key,
|
||||
salt,
|
||||
});
|
||||
while (decKeyBuffs.length > 50) {
|
||||
decKeyBuffs.shift();
|
||||
}
|
||||
return [key, salt];
|
||||
}
|
||||
|
||||
function getSemiStaticField(reset?: boolean) {
|
||||
// return fixed field of iv.
|
||||
if (semiStaticFieldBuffer != null && !reset) {
|
||||
return semiStaticFieldBuffer;
|
||||
}
|
||||
semiStaticFieldBuffer = crypto.getRandomValues(new Uint8Array(12));
|
||||
return semiStaticFieldBuffer;
|
||||
}
|
||||
|
||||
function getNonce() {
|
||||
// This is nonce, so do not send same thing.
|
||||
nonceBuffer[0]++;
|
||||
if (nonceBuffer[0] > 10000) {
|
||||
// reset semi-static field.
|
||||
getSemiStaticField(true);
|
||||
}
|
||||
return nonceBuffer;
|
||||
}
|
||||
|
||||
function uint8ArrayToHexString(src: Uint8Array): string {
|
||||
return Array.from(src)
|
||||
.map((e: number): string => `00${e.toString(16)}`.slice(-2))
|
||||
.join("");
|
||||
}
|
||||
function hexStringToUint8Array(src: string): Uint8Array {
|
||||
const srcArr = [...src];
|
||||
const arr = srcArr.reduce((acc, _, i) => (i % 2 ? acc : [...acc, srcArr.slice(i, i + 2).join("")]), []).map((e) => parseInt(e, 16));
|
||||
return Uint8Array.from(arr);
|
||||
}
|
||||
export async function encrypt(input: string, passphrase: string) {
|
||||
const [key, salt] = await getKeyForEncrypt(passphrase);
|
||||
// Create initial vector with semifixed part and incremental part
|
||||
// I think it's not good against related-key attacks.
|
||||
const fixedPart = getSemiStaticField();
|
||||
const invocationPart = getNonce();
|
||||
const iv = Uint8Array.from([...fixedPart, ...new Uint8Array(invocationPart.buffer)]);
|
||||
const plainStringified: string = JSON.stringify(input);
|
||||
const plainStringBuffer: Uint8Array = new TextEncoder().encode(plainStringified);
|
||||
const encryptedDataArrayBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plainStringBuffer);
|
||||
|
||||
const encryptedData = window.btoa(Array.from(new Uint8Array(encryptedDataArrayBuffer), (char) => String.fromCharCode(char)).join(""));
|
||||
|
||||
//return data with iv and salt.
|
||||
const response: encodedData = [encryptedData, uint8ArrayToHexString(iv), uint8ArrayToHexString(salt)];
|
||||
const ret = JSON.stringify(response);
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function decrypt(encryptedResult: string, passphrase: string): Promise<string> {
|
||||
try {
|
||||
const [encryptedData, ivString, salt]: encodedData = JSON.parse(encryptedResult);
|
||||
const [key] = await getKeyForDecryption(passphrase, hexStringToUint8Array(salt));
|
||||
const iv = hexStringToUint8Array(ivString);
|
||||
// decode base 64, it should increase speed and i should with in MAX_DOC_SIZE_BIN, so it won't OOM.
|
||||
const encryptedDataBin = window.atob(encryptedData);
|
||||
const encryptedDataArrayBuffer = Uint8Array.from(encryptedDataBin.split(""), (char) => char.charCodeAt(0));
|
||||
const plainStringBuffer: ArrayBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encryptedDataArrayBuffer);
|
||||
const plainStringified = new TextDecoder().decode(plainStringBuffer);
|
||||
const plain = JSON.parse(plainStringified);
|
||||
return plain;
|
||||
} catch (ex) {
|
||||
Logger("Couldn't decode! You should wrong the passphrases", LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testCrypt() {
|
||||
const src = "supercalifragilisticexpialidocious";
|
||||
const encoded = await encrypt(src, "passwordTest");
|
||||
const decrypted = await decrypt(encoded, "passwordTest");
|
||||
if (src != decrypted) {
|
||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.VERBOSE);
|
||||
return false;
|
||||
} else {
|
||||
Logger("CRYPT LOGIC OK", LOG_LEVEL.VERBOSE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
1
src/lib
Submodule
1
src/lib
Submodule
Submodule src/lib added at d495f4577a
@@ -1,13 +0,0 @@
|
||||
import { LOG_LEVEL } from "./types";
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
export let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message, _) => {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
||||
const newmessage = timestamp + "->" + messagecontent;
|
||||
console.log(newmessage);
|
||||
};
|
||||
|
||||
export function setLogger(loggerFun: (message: any, levlel?: LOG_LEVEL) => Promise<void>) {
|
||||
Logger = loggerFun;
|
||||
}
|
||||
118
src/main.ts
118
src/main.ts
@@ -1,25 +1,24 @@
|
||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App } from "obsidian";
|
||||
import { 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 } from "./lib/src/types";
|
||||
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList } from "./types";
|
||||
import {
|
||||
EntryDoc,
|
||||
LoadedEntry,
|
||||
ObsidianLiveSyncSettings,
|
||||
diff_check_result,
|
||||
diff_result_leaf,
|
||||
EntryBody,
|
||||
PluginDataEntry,
|
||||
LOG_LEVEL,
|
||||
VER,
|
||||
PERIODIC_PLUGIN_SWEEP,
|
||||
DEFAULT_SETTINGS,
|
||||
PluginList,
|
||||
DevicePluginList,
|
||||
diff_result,
|
||||
FLAGMD_REDFLAG,
|
||||
} from "./types";
|
||||
import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock, shouldBeIgnored, getProcessingCounts, setLockNotifier, isPlainText } from "./utils";
|
||||
import { Logger, setLogger } from "./logger";
|
||||
base64ToString,
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
isValidPath,
|
||||
versionNumberString2Number,
|
||||
runWithLock,
|
||||
shouldBeIgnored,
|
||||
getProcessingCounts,
|
||||
setLockNotifier,
|
||||
isPlainText,
|
||||
setNoticeClass,
|
||||
NewNotice,
|
||||
allSettledWithConcurrencyLimit,
|
||||
} from "./lib/src/utils";
|
||||
import { Logger, setLogger } from "./lib/src/logger";
|
||||
import { LocalPouchDB } from "./LocalPouchDB";
|
||||
import { LogDisplayModal } from "./LogDisplayModal";
|
||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||
@@ -27,7 +26,8 @@ import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
|
||||
import PluginPane from "./PluginPane.svelte";
|
||||
|
||||
import { id2path, path2id } from "./utils";
|
||||
setNoticeClass(Notice);
|
||||
class PluginDialogModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
logEl: HTMLDivElement;
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -702,7 +713,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
|
||||
async doc2storage_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
|
||||
const pathSrc = id2path(docEntry._id);
|
||||
if (shouldBeIgnored(pathSrc)) {
|
||||
return;
|
||||
@@ -781,7 +792,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
} else if (targetFile instanceof TFile) {
|
||||
const doc = change;
|
||||
const file = targetFile;
|
||||
await this.doc2storate_modify(doc, file);
|
||||
await this.doc2storage_modify(doc, file);
|
||||
this.queueConflictedCheck(file);
|
||||
} else {
|
||||
Logger(`${id2path(change._id)} is already exist as the folder`);
|
||||
@@ -847,7 +858,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
a.addEventListener("click", () => this.showPluginSyncModal());
|
||||
});
|
||||
});
|
||||
new Notice(fragment, 10000);
|
||||
NewNotice(fragment, 10000);
|
||||
} else {
|
||||
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -964,14 +975,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
async replicate(showMessage?: boolean) {
|
||||
if (this.settings.versionUpFlash != "") {
|
||||
new Notice("Open settings and check message, please.");
|
||||
NewNotice("Open settings and check message, please.");
|
||||
return;
|
||||
}
|
||||
await this.applyBatchChange();
|
||||
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) {
|
||||
@@ -1002,7 +1013,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
// synchronize all files between database and storage.
|
||||
let notice: Notice = null;
|
||||
if (showingNotice) {
|
||||
notice = new Notice("Initializing", 0);
|
||||
notice = NewNotice("Initializing", 0);
|
||||
}
|
||||
const filesStorage = this.app.vault.getFiles();
|
||||
const filesStorageName = filesStorage.map((e) => e.path);
|
||||
@@ -1024,12 +1035,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger(procedurename);
|
||||
let i = 0;
|
||||
// let lastTicks = performance.now() + 2000;
|
||||
let workProcs = 0;
|
||||
const procs = objects.map(async (e) => {
|
||||
try {
|
||||
workProcs++;
|
||||
await callback(e);
|
||||
i++;
|
||||
if (i % 25 == 0) {
|
||||
const notify = `${procedurename} : ${i}/${count}`;
|
||||
const notify = `${procedurename} : ${workProcs}/${count} (Pending:${workProcs})`;
|
||||
if (notice != null) notice.setMessage(notify);
|
||||
Logger(notify);
|
||||
this.setStatusBarText(notify);
|
||||
@@ -1037,27 +1050,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
} catch (ex) {
|
||||
Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex);
|
||||
} finally {
|
||||
workProcs--;
|
||||
}
|
||||
});
|
||||
// @ts-ignore
|
||||
if (!Promise.allSettled) {
|
||||
await Promise.all(
|
||||
procs.map((p) =>
|
||||
p
|
||||
.then((value) => ({
|
||||
status: "fulfilled",
|
||||
value,
|
||||
}))
|
||||
.catch((reason) => ({
|
||||
status: "rejected",
|
||||
reason,
|
||||
}))
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
await Promise.allSettled(procs);
|
||||
}
|
||||
|
||||
await allSettledWithConcurrencyLimit(procs, 10);
|
||||
};
|
||||
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
||||
Logger(`Update into ${e.path}`);
|
||||
@@ -1329,7 +1327,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const file = targetFile;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
if (doc === false) return;
|
||||
await this.doc2storate_modify(doc, file, force);
|
||||
await this.doc2storage_modify(doc, file, force);
|
||||
} else {
|
||||
Logger(`target files:${filename} is exists as the folder`);
|
||||
//something went wrong..
|
||||
@@ -1354,7 +1352,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger(`${storageMtime} < ${docMtime}`);
|
||||
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
||||
if (docx != false) {
|
||||
await this.doc2storate_modify(docx, file);
|
||||
await this.doc2storage_modify(docx, file);
|
||||
}
|
||||
} else {
|
||||
// Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE);
|
||||
@@ -1471,7 +1469,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
Logger("Sweeping plugins", logLevel);
|
||||
Logger("Scanning plugins", logLevel);
|
||||
const db = this.localDatabase.localDatabase;
|
||||
const oldDocs = await db.allDocs({
|
||||
startkey: `ps:${this.deviceAndVaultName}-`,
|
||||
@@ -1543,7 +1541,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return e.doc;
|
||||
});
|
||||
await db.bulkDocs(delDocs);
|
||||
Logger(`Sweep plugin done.`, logLevel);
|
||||
Logger(`Scan plugin done.`, logLevel);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
4
src/pouchdb-browser.ts
Normal file
4
src/pouchdb-browser.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PouchDB as PouchDB_ } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
||||
|
||||
const Pouch: PouchDB.Static = PouchDB_;
|
||||
export { Pouch as PouchDB };
|
||||
217
src/types.ts
217
src/types.ts
@@ -1,154 +1,7 @@
|
||||
// docs should be encoded as base64, so 1 char -> 1 bytes
|
||||
// and cloudant limitation is 1MB , we use 900kb;
|
||||
|
||||
import { PluginManifest } from "obsidian";
|
||||
import * as PouchDB from "pouchdb";
|
||||
import { DatabaseEntry } from "./lib/src/types";
|
||||
|
||||
export const MAX_DOC_SIZE = 1000; // for .md file, but if delimiters exists. use that before.
|
||||
export const MAX_DOC_SIZE_BIN = 102400; // 100kb
|
||||
export const VER = 10;
|
||||
|
||||
export const RECENT_MOFIDIED_DOCS_QTY = 30;
|
||||
export const LEAF_WAIT_TIMEOUT = 90000; // in synchronization, waiting missing leaf time out.
|
||||
export const LOG_LEVEL = {
|
||||
VERBOSE: 1,
|
||||
INFO: 10,
|
||||
NOTICE: 100,
|
||||
URGENT: 1000,
|
||||
} as const;
|
||||
export type LOG_LEVEL = typeof LOG_LEVEL[keyof typeof LOG_LEVEL];
|
||||
export const VERSIONINFO_DOCID = "obsydian_livesync_version";
|
||||
export const MILSTONE_DOCID = "_local/obsydian_livesync_milestone";
|
||||
export const NODEINFO_DOCID = "_local/obsydian_livesync_nodeinfo";
|
||||
|
||||
export interface ObsidianLiveSyncSettings {
|
||||
couchDB_URI: string;
|
||||
couchDB_USER: string;
|
||||
couchDB_PASSWORD: string;
|
||||
couchDB_DBNAME: string;
|
||||
liveSync: boolean;
|
||||
syncOnSave: boolean;
|
||||
syncOnStart: boolean;
|
||||
syncOnFileOpen: boolean;
|
||||
savingDelay: number;
|
||||
lessInformationInLog: boolean;
|
||||
gcDelay: number;
|
||||
versionUpFlash: string;
|
||||
minimumChunkSize: number;
|
||||
longLineThreshold: number;
|
||||
showVerboseLog: boolean;
|
||||
suspendFileWatching: boolean;
|
||||
trashInsteadDelete: boolean;
|
||||
periodicReplication: boolean;
|
||||
periodicReplicationInterval: number;
|
||||
encrypt: boolean;
|
||||
passphrase: string;
|
||||
workingEncrypt: boolean;
|
||||
workingPassphrase: string;
|
||||
doNotDeleteFolder: boolean;
|
||||
resolveConflictsByNewerFile: boolean;
|
||||
batchSave: boolean;
|
||||
deviceAndVaultName: string;
|
||||
usePluginSettings: boolean;
|
||||
showOwnPlugins: boolean;
|
||||
showStatusOnEditor: boolean;
|
||||
usePluginSync: boolean;
|
||||
autoSweepPlugins: boolean;
|
||||
autoSweepPluginsPeriodic: boolean;
|
||||
notifyPluginOrSettingUpdated: boolean;
|
||||
checkIntegrityOnSave: boolean;
|
||||
batch_size: number;
|
||||
batches_limit: number;
|
||||
useHistory: boolean;
|
||||
disableRequestURI: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
||||
couchDB_URI: "",
|
||||
couchDB_USER: "",
|
||||
couchDB_PASSWORD: "",
|
||||
couchDB_DBNAME: "",
|
||||
liveSync: false,
|
||||
syncOnSave: false,
|
||||
syncOnStart: false,
|
||||
savingDelay: 200,
|
||||
lessInformationInLog: false,
|
||||
gcDelay: 300,
|
||||
versionUpFlash: "",
|
||||
minimumChunkSize: 20,
|
||||
longLineThreshold: 250,
|
||||
showVerboseLog: false,
|
||||
suspendFileWatching: false,
|
||||
trashInsteadDelete: true,
|
||||
periodicReplication: false,
|
||||
periodicReplicationInterval: 60,
|
||||
syncOnFileOpen: false,
|
||||
encrypt: false,
|
||||
passphrase: "",
|
||||
workingEncrypt: false,
|
||||
workingPassphrase: "",
|
||||
doNotDeleteFolder: false,
|
||||
resolveConflictsByNewerFile: false,
|
||||
batchSave: false,
|
||||
deviceAndVaultName: "",
|
||||
usePluginSettings: false,
|
||||
showOwnPlugins: false,
|
||||
showStatusOnEditor: false,
|
||||
usePluginSync: false,
|
||||
autoSweepPlugins: false,
|
||||
autoSweepPluginsPeriodic: false,
|
||||
notifyPluginOrSettingUpdated: false,
|
||||
checkIntegrityOnSave: false,
|
||||
batch_size: 250,
|
||||
batches_limit: 40,
|
||||
useHistory: false,
|
||||
disableRequestURI: false,
|
||||
};
|
||||
|
||||
export const PERIODIC_PLUGIN_SWEEP = 60;
|
||||
|
||||
export interface Entry {
|
||||
_id: string;
|
||||
data: string;
|
||||
_rev?: string;
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
size: number;
|
||||
_deleted?: boolean;
|
||||
_conflicts?: string[];
|
||||
type?: "notes";
|
||||
}
|
||||
export interface NewEntry {
|
||||
_id: string;
|
||||
children: string[];
|
||||
_rev?: string;
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
size: number;
|
||||
_deleted?: boolean;
|
||||
_conflicts?: string[];
|
||||
NewNote: true;
|
||||
type: "newnote";
|
||||
}
|
||||
export interface PlainEntry {
|
||||
_id: string;
|
||||
children: string[];
|
||||
_rev?: string;
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
size: number;
|
||||
_deleted?: boolean;
|
||||
NewNote: true;
|
||||
_conflicts?: string[];
|
||||
type: "plain";
|
||||
}
|
||||
export type LoadedEntry = Entry & {
|
||||
children: string[];
|
||||
datatype: "plain" | "newnote";
|
||||
};
|
||||
|
||||
export interface PluginDataEntry {
|
||||
_id: string;
|
||||
export interface PluginDataEntry extends DatabaseEntry {
|
||||
deviceVaultName: string;
|
||||
mtime: number;
|
||||
manifest: PluginManifest;
|
||||
@@ -157,73 +10,10 @@ export interface PluginDataEntry {
|
||||
styleCss?: string;
|
||||
// it must be encrypted.
|
||||
dataJson?: string;
|
||||
_rev?: string;
|
||||
_deleted?: boolean;
|
||||
_conflicts?: string[];
|
||||
type: "plugin";
|
||||
}
|
||||
|
||||
export interface EntryLeaf {
|
||||
_id: string;
|
||||
data: string;
|
||||
_deleted?: boolean;
|
||||
type: "leaf";
|
||||
_rev?: string;
|
||||
}
|
||||
|
||||
export interface EntryVersionInfo {
|
||||
_id: typeof VERSIONINFO_DOCID;
|
||||
_rev?: string;
|
||||
type: "versioninfo";
|
||||
version: number;
|
||||
_deleted?: boolean;
|
||||
}
|
||||
|
||||
export interface EntryMilestoneInfo {
|
||||
_id: typeof MILSTONE_DOCID;
|
||||
_rev?: string;
|
||||
type: "milestoneinfo";
|
||||
_deleted?: boolean;
|
||||
created: number;
|
||||
accepted_nodes: string[];
|
||||
locked: boolean;
|
||||
}
|
||||
|
||||
export interface EntryNodeInfo {
|
||||
_id: typeof NODEINFO_DOCID;
|
||||
_rev?: string;
|
||||
_deleted?: boolean;
|
||||
type: "nodeinfo";
|
||||
nodeid: string;
|
||||
}
|
||||
|
||||
export type EntryBody = Entry | NewEntry | PlainEntry;
|
||||
export type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo;
|
||||
|
||||
export type diff_result_leaf = {
|
||||
rev: string;
|
||||
data: string;
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
};
|
||||
export type dmp_result = Array<[number, string]>;
|
||||
|
||||
export type diff_result = {
|
||||
left: diff_result_leaf;
|
||||
right: diff_result_leaf;
|
||||
diff: dmp_result;
|
||||
};
|
||||
export type diff_check_result = boolean | diff_result;
|
||||
|
||||
export type Credential = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;
|
||||
|
||||
export type DatabaseConnectingStatus = "STARTED" | "NOT_CONNECTED" | "PAUSED" | "CONNECTED" | "COMPLETED" | "CLOSED" | "ERRORED";
|
||||
|
||||
export interface PluginList {
|
||||
[key: string]: PluginDataEntry[];
|
||||
}
|
||||
@@ -231,5 +21,4 @@ export interface PluginList {
|
||||
export interface DevicePluginList {
|
||||
[key: string]: PluginDataEntry;
|
||||
}
|
||||
|
||||
export const FLAGMD_REDFLAG = "redflag.md";
|
||||
export const PERIODIC_PLUGIN_SWEEP = 60;
|
||||
|
||||
243
src/utils.ts
243
src/utils.ts
@@ -1,249 +1,14 @@
|
||||
import { normalizePath } from "obsidian";
|
||||
import { Logger } from "./logger";
|
||||
import { FLAGMD_REDFLAG, LOG_LEVEL } from "./types";
|
||||
|
||||
export function arrayBufferToBase64(buffer: ArrayBuffer): Promise<string> {
|
||||
return new Promise((res) => {
|
||||
const blob = new Blob([buffer], { type: "application/octet-binary" });
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (evt) {
|
||||
const dataurl = evt.target.result.toString();
|
||||
res(dataurl.substr(dataurl.indexOf(",") + 1));
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export function base64ToString(base64: string): string {
|
||||
try {
|
||||
const binary_string = window.atob(base64);
|
||||
const len = binary_string.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder().decode(bytes);
|
||||
} catch (ex) {
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
try {
|
||||
const binary_string = window.atob(base64);
|
||||
const len = binary_string.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
} catch (ex) {
|
||||
try {
|
||||
return new Uint16Array(
|
||||
[].map.call(base64, function (c: string) {
|
||||
return c.charCodeAt(0);
|
||||
})
|
||||
).buffer;
|
||||
} catch (ex2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const escapeStringToHTML = (str: string) => {
|
||||
if (!str) return "";
|
||||
return str.replace(/[<>&"'`]/g, (match) => {
|
||||
const escape: any = {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`",
|
||||
};
|
||||
return escape[match];
|
||||
});
|
||||
};
|
||||
|
||||
export function resolveWithIgnoreKnownError<T>(p: Promise<T>, def: T): Promise<T> {
|
||||
return new Promise((res, rej) => {
|
||||
p.then(res).catch((ex) => ((ex.status && ex.status == 404) || (ex.message && ex.message == "Request Error:404") ? res(def) : rej(ex)));
|
||||
});
|
||||
}
|
||||
|
||||
export function isValidPath(filename: string): boolean {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const regex = /[\u0000-\u001f]|[\\":?<>|*#]/g;
|
||||
let x = filename.replace(regex, "_");
|
||||
const win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi;
|
||||
const sx = (x = x.replace(win, "/_"));
|
||||
return sx == filename;
|
||||
}
|
||||
|
||||
export function shouldBeIgnored(filename: string): boolean {
|
||||
if (filename == FLAGMD_REDFLAG) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function versionNumberString2Number(version: string): number {
|
||||
return version // "1.23.45"
|
||||
.split(".") // 1 23 45
|
||||
.reverse() // 45 23 1
|
||||
.map((e, i) => ((e as any) / 1) * 1000 ** i) // 45 23000 1000000
|
||||
.reduce((prev, current) => prev + current, 0); // 1023045
|
||||
}
|
||||
|
||||
export const delay = (ms: number): Promise<void> => {
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
res();
|
||||
}, ms);
|
||||
});
|
||||
};
|
||||
import { path2id_base, id2path_base } from "./lib/src/utils";
|
||||
|
||||
// For backward compatibility, using the path for determining id.
|
||||
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
// The first slash will be deleted when the path is normalized.
|
||||
export function path2id(filename: string): string {
|
||||
let x = normalizePath(filename);
|
||||
if (x.startsWith("_")) x = "/" + x;
|
||||
return x;
|
||||
const x = normalizePath(filename);
|
||||
return path2id_base(x);
|
||||
}
|
||||
export function id2path(filename: string): string {
|
||||
return normalizePath(filename);
|
||||
}
|
||||
|
||||
const runningProcs: string[] = [];
|
||||
const pendingProcs: { [key: string]: (() => Promise<void>)[] } = {};
|
||||
function objectToKey(key: any): string {
|
||||
if (typeof key === "string") return key;
|
||||
const keys = Object.keys(key).sort((a, b) => a.localeCompare(b));
|
||||
return keys.map((e) => e + objectToKey(key[e])).join(":");
|
||||
}
|
||||
export function getProcessingCounts() {
|
||||
let count = 0;
|
||||
for (const v in pendingProcs) {
|
||||
count += pendingProcs[v].length;
|
||||
}
|
||||
count += runningProcs.length;
|
||||
return count;
|
||||
}
|
||||
|
||||
let externalNotifier: () => void = () => {};
|
||||
let notifyTimer: number = null;
|
||||
export function setLockNotifier(fn: () => void) {
|
||||
externalNotifier = fn;
|
||||
}
|
||||
function notifyLock() {
|
||||
if (notifyTimer != null) {
|
||||
window.clearTimeout(notifyTimer);
|
||||
}
|
||||
notifyTimer = window.setTimeout(() => {
|
||||
externalNotifier();
|
||||
}, 100);
|
||||
}
|
||||
// Just run async/await as like transacion ISOLATION SERIALIZABLE
|
||||
export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: () => Promise<T>): Promise<T> {
|
||||
// Logger(`Lock:${key}:enter`, LOG_LEVEL.VERBOSE);
|
||||
const lockKey = typeof key === "string" ? key : objectToKey(key);
|
||||
const handleNextProcs = () => {
|
||||
if (typeof pendingProcs[lockKey] === "undefined") {
|
||||
//simply unlock
|
||||
runningProcs.remove(lockKey);
|
||||
notifyLock();
|
||||
// Logger(`Lock:${lockKey}:released`, LOG_LEVEL.VERBOSE);
|
||||
} else {
|
||||
Logger(`Lock:${lockKey}:left ${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
||||
let nextProc = null;
|
||||
nextProc = pendingProcs[lockKey].shift();
|
||||
notifyLock();
|
||||
if (nextProc) {
|
||||
// left some
|
||||
nextProc()
|
||||
.then()
|
||||
.catch((err) => {
|
||||
Logger(err);
|
||||
})
|
||||
.finally(() => {
|
||||
if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) {
|
||||
delete pendingProcs[lockKey];
|
||||
notifyLock();
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
handleNextProcs();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) {
|
||||
delete pendingProcs[lockKey];
|
||||
notifyLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (runningProcs.contains(lockKey)) {
|
||||
if (ignoreWhenRunning) {
|
||||
return null;
|
||||
}
|
||||
if (typeof pendingProcs[lockKey] === "undefined") {
|
||||
pendingProcs[lockKey] = [];
|
||||
}
|
||||
let responderRes: (value: T | PromiseLike<T>) => void;
|
||||
let responderRej: (reason?: unknown) => void;
|
||||
const responder = new Promise<T>((res, rej) => {
|
||||
responderRes = res;
|
||||
responderRej = rej;
|
||||
//wait for subproc resolved
|
||||
});
|
||||
const subproc = () =>
|
||||
new Promise<void>((res, rej) => {
|
||||
proc()
|
||||
.then((v) => {
|
||||
// Logger(`Lock:${key}:processed`, LOG_LEVEL.VERBOSE);
|
||||
handleNextProcs();
|
||||
responderRes(v);
|
||||
res();
|
||||
})
|
||||
.catch((reason) => {
|
||||
Logger(`Lock:${key}:rejected`, LOG_LEVEL.VERBOSE);
|
||||
handleNextProcs();
|
||||
rej(reason);
|
||||
responderRej(reason);
|
||||
});
|
||||
});
|
||||
|
||||
pendingProcs[lockKey].push(subproc);
|
||||
notifyLock();
|
||||
// Logger(`Lock:${lockKey}:queud:left${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
||||
return responder;
|
||||
} else {
|
||||
runningProcs.push(lockKey);
|
||||
notifyLock();
|
||||
// Logger(`Lock:${lockKey}:aqquired`, LOG_LEVEL.VERBOSE);
|
||||
return new Promise((res, rej) => {
|
||||
proc()
|
||||
.then((v) => {
|
||||
handleNextProcs();
|
||||
res(v);
|
||||
})
|
||||
.catch((reason) => {
|
||||
handleNextProcs();
|
||||
rej(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function isPlainText(filename: string): boolean {
|
||||
if (filename.endsWith(".md")) return true;
|
||||
if (filename.endsWith(".txt")) return true;
|
||||
if (filename.endsWith(".svg")) return true;
|
||||
if (filename.endsWith(".html")) return true;
|
||||
if (filename.endsWith(".csv")) return true;
|
||||
if (filename.endsWith(".css")) return true;
|
||||
if (filename.endsWith(".js")) return true;
|
||||
if (filename.endsWith(".xml")) return true;
|
||||
|
||||
return false;
|
||||
return id2path_base(normalizePath(filename));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Logger } from "./logger";
|
||||
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc } from "./types";
|
||||
import { resolveWithIgnoreKnownError } from "./utils";
|
||||
import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
||||
import { requestUrl, RequestUrlParam } from "obsidian";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc } from "./lib/src/types";
|
||||
import { resolveWithIgnoreKnownError } from "./lib/src/utils";
|
||||
import { PouchDB } from "./pouchdb-browser";
|
||||
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||
|
||||
export const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
||||
if (uri.startsWith("https://")) return true;
|
||||
@@ -13,6 +13,19 @@ let last_post_successed = false;
|
||||
export const getLastPostFailedBySize = () => {
|
||||
return !last_post_successed;
|
||||
};
|
||||
const fetchByAPI = async (request: RequestUrlParam): Promise<RequestUrlResponse> => {
|
||||
const ret = await requestUrl(request);
|
||||
if (ret.status - (ret.status % 100) !== 200) {
|
||||
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
|
||||
if (ret.json) {
|
||||
er.message = ret.json.reason;
|
||||
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
|
||||
}
|
||||
er.status = ret.status;
|
||||
throw er;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }, disableRequestURI: boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> => {
|
||||
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
||||
@@ -28,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";
|
||||
@@ -36,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);
|
||||
@@ -65,15 +76,12 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
||||
};
|
||||
|
||||
try {
|
||||
const r = await requestUrl(requestParam);
|
||||
const r = await fetchByAPI(requestParam);
|
||||
if (method == "POST" || method == "PUT") {
|
||||
last_post_successed = r.status - (r.status % 100) == 200;
|
||||
} else {
|
||||
last_post_successed = true;
|
||||
}
|
||||
if (r.status - (r.status % 100) !== 200) {
|
||||
throw new Error(`Request Error:${r.status}`);
|
||||
}
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL.VERBOSE);
|
||||
|
||||
return new Response(r.arrayBuffer, {
|
||||
@@ -83,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);
|
||||
@@ -104,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);
|
||||
@@ -146,7 +156,7 @@ export const checkRemoteVersion = async (db: PouchDB.Database, migrate: (from: n
|
||||
if (version == barrier) return true;
|
||||
return false;
|
||||
} catch (ex) {
|
||||
if ((ex.status && ex.status == 404) || (ex.message && ex.message == "Request Error:404")) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
if (await bumpRemoteVersion(db)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
16
styles.css
16
styles.css
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"types": ["svelte", "node"],
|
||||
// "importsNotUsedAsValues": "error",
|
||||
"importHelpers": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
Reference in New Issue
Block a user