Refactored and touched up some.

Not available on iOS yet, be careful!
This commit is contained in:
vorotamoroz
2022-04-28 18:24:48 +09:00
parent 9d3aa35b0b
commit a50f0965f6
27 changed files with 807 additions and 10681 deletions

View File

@@ -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]>;

View File

@@ -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;

View File

@@ -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,18 @@ 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";
export class LocalPouchDB {
auth: Credential;
dbname: string;
settings: ObsidianLiveSyncSettings;
settings: RemoteDBSettings;
localDatabase: PouchDB.Database<EntryDoc>;
nodeid = "";
isReady = false;
@@ -77,7 +77,7 @@ export class LocalPouchDB {
this.localDatabase.removeAllListeners();
}
constructor(settings: ObsidianLiveSyncSettings, dbname: string) {
constructor(settings: RemoteDBSettings, dbname: string) {
this.auth = {
username: "",
password: "",
@@ -503,7 +503,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 +516,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 +615,7 @@ export class LocalPouchDB {
}
}
savenNotes.push(leafid);
} while (leftData != "");
}
let saved = true;
if (newLeafs.length > 0) {
try {
@@ -727,14 +696,14 @@ 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;
let notice: WrappedNotice = null;
if (showingNotice) {
notice = new Notice("Initializing", 0);
notice = NewNotice("Initializing", 0);
}
this.syncStatus = "STARTED";
this.updateInfo();
@@ -800,7 +769,7 @@ export class LocalPouchDB {
});
}
async checkReplicationConnectivity(setting: ObsidianLiveSyncSettings, keepAlive: boolean, skipCheck: boolean) {
async checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean) {
if (!this.isReady) {
Logger("Database is not ready.");
return false;
@@ -808,7 +777,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);
@@ -839,15 +808,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,20 +830,20 @@ 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> {
async openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>): Promise<boolean> {
return await runWithLock("replicate", false, () => {
return this._openReplication(setting, keepAlive, showResult, callback, false);
});
}
originalSetting: ObsidianLiveSyncSettings = null;
originalSetting: RemoteDBSettings = 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> {
async _openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>, retrying: boolean): Promise<boolean> {
const ret = await this.checkReplicationConnectivity(setting, keepAlive, retrying);
if (ret === false) return false;
let notice: Notice = null;
let notice: WrappedNotice = null;
if (showResult) {
notice = new Notice("Looking for the point last synchronized point.", 0);
notice = NewNotice("Looking for the point last synchronized point.", 0);
}
const { db, syncOptionBase, syncOption } = ret;
//replicate once
@@ -919,12 +879,10 @@ export class LocalPouchDB {
.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) {
@@ -974,7 +932,7 @@ export class LocalPouchDB {
Logger("Replication stopped.", LOG_LEVEL.NOTICE);
} else {
// Duplicate settings for smaller batch.
const xsetting: ObsidianLiveSyncSettings = JSON.parse(JSON.stringify(setting));
const xsetting: RemoteDBSettings = JSON.parse(JSON.stringify(setting));
xsetting.batch_size = Math.ceil(xsetting.batch_size / 2);
xsetting.batches_limit = Math.ceil(xsetting.batches_limit / 2);
if (xsetting.batch_size <= 3 || xsetting.batches_limit <= 3) {
@@ -1074,7 +1032,7 @@ 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 = {
@@ -1092,7 +1050,7 @@ 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 = {
@@ -1103,7 +1061,7 @@ export class LocalPouchDB {
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,
@@ -1137,7 +1095,7 @@ 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,

View File

@@ -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 {

View File

@@ -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 } 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 {
@@ -688,7 +689,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 +715,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 +835,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 +845,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 +856,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;

View File

@@ -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">-->

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
@@ -702,7 +702,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 +781,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 +847,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);
}
@@ -964,7 +964,7 @@ 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();
@@ -1002,7 +1002,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 +1024,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 +1039,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 +1316,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 +1341,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 +1458,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 +1530,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return e.doc;
});
await db.bulkDocs(delDocs);
Logger(`Sweep plugin done.`, logLevel);
Logger(`Scan plugin done.`, logLevel);
});
}

9
src/pouchdb-browser.ts Normal file
View File

@@ -0,0 +1,9 @@
import PouchDB from "pouchdb-core";
import IDBPouch from "pouchdb-adapter-idb";
import HttpPouch from "pouchdb-adapter-http";
import mapreduce from "pouchdb-mapreduce";
import replication from "pouchdb-replication";
PouchDB.plugin(IDBPouch).plugin(HttpPouch).plugin(mapreduce).plugin(replication);
export { PouchDB };

View File

@@ -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;

View File

@@ -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 = {
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
'"': "&quot;",
"'": "&#39;",
"`": "&#x60;",
};
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 ? 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));
}

View File

@@ -1,7 +1,7 @@
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 { 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 => {