diff --git a/manifest.json b/manifest.json index a8d56a5..13ad073 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.10.1", + "version": "0.11.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", diff --git a/package-lock.json b/package-lock.json index 4f2ed0c..3a05855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.10.1", + "version": "0.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.10.1", + "version": "0.11.0", "license": "MIT", "dependencies": { "diff-match-patch": "^1.0.5", diff --git a/package.json b/package.json index d510374..9e35518 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.10.1", + "version": "0.11.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", diff --git a/src/LocalPouchDB.ts b/src/LocalPouchDB.ts index 5777a4b..a3da860 100644 --- a/src/LocalPouchDB.ts +++ b/src/LocalPouchDB.ts @@ -16,7 +16,6 @@ import { MAX_DOC_SIZE, MAX_DOC_SIZE_BIN, NODEINFO_DOCID, - RECENT_MOFIDIED_DOCS_QTY, VER, MILSTONE_DOCID, DatabaseConnectingStatus, @@ -142,19 +141,6 @@ export class LocalPouchDB { } } - updateRecentModifiedDocs(id: string, rev: string, deleted: boolean) { - const idrev = id + rev; - if (deleted) { - this.recentModifiedDocs = this.recentModifiedDocs.filter((e) => e != idrev); - } else { - this.recentModifiedDocs.push(idrev); - this.recentModifiedDocs = this.recentModifiedDocs.slice(0 - RECENT_MOFIDIED_DOCS_QTY); - } - } - isSelfModified(id: string, rev: string): boolean { - const idrev = id + rev; - return this.recentModifiedDocs.indexOf(idrev) !== -1; - } async isOldDatabaseExists() { const db = new PouchDB(this.dbname + "-livesync", { auto_compaction: this.settings.useHistory ? false : true, @@ -305,7 +291,7 @@ export class LocalPouchDB { waitForLeafReady(id: string): Promise { return new Promise((res, rej) => { // Set timeout. - const timer = setTimeout(() => rej(new Error(`Leaf timed out:${id}`)), LEAF_WAIT_TIMEOUT); + const timer = setTimeout(() => rej(new Error(`Chunk reading timed out:${id}`)), LEAF_WAIT_TIMEOUT); if (typeof this.leafArrivedCallbacks[id] == "undefined") { this.leafArrivedCallbacks[id] = []; } @@ -329,21 +315,21 @@ export class LocalPouchDB { this.hashCaches.set(id, w.data); return w.data; } - throw new Error(`retrive leaf, but it was not leaf.`); + throw new Error(`Corrupted chunk detected.`); } catch (ex) { if (ex.status && ex.status == 404) { if (waitForReady) { // just leaf is not ready. // wait for on if ((await this.waitForLeafReady(id)) === false) { - throw new Error(`time out (waiting leaf)`); + throw new Error(`time out (waiting chunk)`); } return this.getDBLeaf(id, false); } else { - throw new Error("Leaf was not found"); + throw new Error("Chunk was not found"); } } else { - Logger(`Something went wrong on retriving leaf`); + Logger(`Something went wrong on retriving chunk`); throw ex; } } @@ -447,11 +433,11 @@ export class LocalPouchDB { try { childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady))); if (dump) { - Logger(`childrens:`); + Logger(`Chunks:`); Logger(childrens); } } catch (ex) { - Logger(`Something went wrong on reading elements of ${obj._id} from database:`, LOG_LEVEL.NOTICE); + Logger(`Something went wrong on reading chunks of ${obj._id} from database:`, LOG_LEVEL.NOTICE); Logger(ex, LOG_LEVEL.VERBOSE); this.corruptedEntries[obj._id] = obj; return false; @@ -515,7 +501,7 @@ export class LocalPouchDB { if (!obj.type || (obj.type && obj.type == "notes")) { obj._deleted = true; const r = await this.localDatabase.put(obj); - this.updateRecentModifiedDocs(r.id, r.rev, true); + Logger(`entry removed:${obj._id}-${r.rev}`); if (typeof this.corruptedEntries[obj._id] != "undefined") { delete this.corruptedEntries[obj._id]; } @@ -526,7 +512,6 @@ export class LocalPouchDB { obj._deleted = true; const r = await this.localDatabase.put(obj); Logger(`entry removed:${obj._id}-${r.rev}`); - this.updateRecentModifiedDocs(r.id, r.rev, true); if (typeof this.corruptedEntries[obj._id] != "undefined") { delete this.corruptedEntries[obj._id]; } @@ -579,7 +564,6 @@ export class LocalPouchDB { const item = await this.localDatabase.get(v); item._deleted = true; await this.localDatabase.put(item); - this.updateRecentModifiedDocs(item._id, item._rev, true); }); deleteCount++; @@ -702,21 +686,21 @@ export class LocalPouchDB { try { const result = await this.localDatabase.bulkDocs(newLeafs); for (const item of result) { - if ((item as any).ok) { - this.updateRecentModifiedDocs(item.id, item.rev, false); - Logger(`save ok:id:${item.id} rev:${item.rev}`, LOG_LEVEL.VERBOSE); - } else { + if (!(item as any).ok) { if ((item as any).status && (item as any).status == 409) { // conflicted, but it would be ok in childrens. } else { - Logger(`save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE); + Logger(`Save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE); Logger(item); saved = false; } } } + if (saved) { + Logger(`Chunk saved:${newLeafs.length} chunks`); + } } catch (ex) { - Logger("ERROR ON SAVING LEAVES:", LOG_LEVEL.NOTICE); + Logger("Chunk save failed:", LOG_LEVEL.NOTICE); Logger(ex, LOG_LEVEL.NOTICE); saved = false; } @@ -748,7 +732,6 @@ export class LocalPouchDB { } } const r = await this.localDatabase.put(newDoc, { force: true }); - this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted); if (typeof this.corruptedEntries[note._id] != "undefined") { delete this.corruptedEntries[note._id]; } diff --git a/src/main.ts b/src/main.ts index 1123491..0470b4d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App } from "obsidian"; +import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal } 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, SYNCINFO_ID } from "./lib/src/types"; @@ -28,6 +28,7 @@ import { DocumentHistoryModal } from "./DocumentHistoryModal"; import PluginPane from "./PluginPane.svelte"; import { id2path, path2id } from "./utils"; +import { decrypt, encrypt } from "./lib/src/e2ee"; const isDebug = false; setNoticeClass(Notice); class PluginDialogModal extends Modal { @@ -57,6 +58,45 @@ class PluginDialogModal extends Modal { } } } +class PopoverYesNo extends FuzzySuggestModal { + app: App; + callback: (e: string) => void = () => {}; + + constructor(app: App, note: string, callback: (e: string) => void) { + super(app); + this.app = app; + this.setPlaceholder("y/n) " + note); + this.callback = callback; + } + + getItems(): string[] { + return ["yes", "no"]; + } + + getItemText(item: string): string { + return item; + } + + onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void { + // debugger; + this.callback(item); + this.callback = null; + } + onClose(): void { + setTimeout(() => { + if (this.callback != null) { + this.callback(""); + } + }, 100); + } +} + +const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => { + return new Promise((res) => { + const popover = new PopoverYesNo(app, message, (result) => res(result as "yes" | "no")); + popover.open(); + }); +}; export default class ObsidianLiveSyncPlugin extends Plugin { settings: ObsidianLiveSyncSettings; @@ -200,6 +240,81 @@ export default class ObsidianLiveSyncPlugin extends Plugin { Logger(ex, LOG_LEVEL.VERBOSE); } }); + this.addCommand({ + id: "livesync-exportconfig", + name: "Copy setup uri (beta)", + callback: async () => { + const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(this.settings), "---")); + const uri = `obsidian://setuplivesync?settings=${encryptedSetting}`; + await navigator.clipboard.writeText(uri); + Logger("Setup uri copied to clipboard", LOG_LEVEL.NOTICE); + }, + }); + this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => { + try { + const oldConf = JSON.parse(JSON.stringify(this.settings)); + const newconf = await JSON.parse(await decrypt(conf.settings, "---")); + if (newconf) { + const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?"); + if (result == "yes") { + const newSettingW = Object.assign({}, this.settings, newconf); + // stopping once. + this.localDatabase.closeReplication(); + this.settings.suspendFileWatching = true; + console.dir(newSettingW); + const keepLocalDB = await askYesNo(this.app, "Keep local DB?"); + const keepRemoteDB = await askYesNo(this.app, "Keep remote DB?"); + if (keepLocalDB == "yes" && keepRemoteDB == "yes") { + // nothing to do. so peaceful. + this.settings = newSettingW; + await this.saveSettings(); + Logger("Configuration loaded.", LOG_LEVEL.NOTICE); + return; + } + if (keepLocalDB == "no" && keepRemoteDB == "no") { + const reset = await askYesNo(this.app, "Drop everything?"); + if (reset != "yes") { + Logger("Cancelled", LOG_LEVEL.NOTICE); + this.settings = oldConf; + return; + } + } + let initDB; + await this.saveSettings(); + if (keepLocalDB == "no") { + this.resetLocalOldDatabase(); + this.resetLocalDatabase(); + this.localDatabase.initializeDatabase(); + const rebuild = await askYesNo(this.app, "Rebuild the database?"); + if (rebuild == "yes") { + initDB = this.initializeDatabase(true); + } else { + this.markRemoteResolved(); + } + } + if (keepRemoteDB == "no") { + await this.tryResetRemoteDatabase(); + await this.markRemoteLocked(); + } + if (keepLocalDB == "no" || keepRemoteDB == "no") { + const replicate = await askYesNo(this.app, "Replicate once?"); + if (replicate == "yes") { + if (initDB != null) { + await initDB; + } + await this.replicate(true); + } + } + } + + Logger("Configuration loaded.", LOG_LEVEL.NOTICE); + } else { + Logger("Cancelled.", LOG_LEVEL.NOTICE); + } + } catch (ex) { + Logger("Couldn't parse configuration uri.", LOG_LEVEL.NOTICE); + } + }); this.addCommand({ id: "livesync-replicate", name: "Replicate now", @@ -587,8 +702,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin { //--> Basic document Functions notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {}; - // eslint-disable-next-line require-await lastLog = ""; + // eslint-disable-next-line require-await async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) { if (level == LOG_LEVEL.DEBUG && !isDebug) { return;