From 3545ae9690d603be29d44e1221668478d2bb8624 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 1 Apr 2022 17:57:14 +0900 Subject: [PATCH] Implemented: - using Obsidian API to synchronize. - Copy button on history dialog. Documented: - Document improved. --- docs/settings.md | 34 ++++++++++++++++++ manifest.json | 2 +- package-lock.json | 18 +++++----- package.json | 4 +-- src/DocumentHistoryModal.ts | 13 +++++++ src/LocalPouchDB.ts | 12 +++---- src/ObsidianLiveSyncSettingTab.ts | 26 ++++++++------ src/types.ts | 2 ++ src/utils_couchdb.ts | 60 ++++++++++++++++++++++++++++++- 9 files changed, 141 insertions(+), 30 deletions(-) diff --git a/docs/settings.md b/docs/settings.md index 7134b21..5501ad9 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -18,7 +18,14 @@ Note: This password is saved into your Obsidian's vault in plain text. The Database name to synchronize. ⚠️If not exist, created automatically. + +### Use the old connecting method +Since v0.8.0, Self-hosted LiveSync uses Obsidian's API to connect to the CouchDB instead of the browser API. +This method will increase the performance and avoid troubles with the CORS. +But it doesn't been well tested yet. If you are troubled, please disable this option once. + ### Test Database connection +You can check the connection by clicking this button. ## Local Database Configurations "Local Database" is created inside your obsidian. @@ -44,6 +51,8 @@ As a result, Obsidian's behavior is temporarily slowed down. Default is 300 seconds. If you are an early adopter, maybe this value is left as 30 seconds. Please change this value to larger values. +Note: If you want to use "Use history", this vault must be set to 0. + ### Manual Garbage Collect Run "Garbage Collection" manually. @@ -52,6 +61,8 @@ Encrypt your database. It affects only the database, your files are left as plai The encryption algorithm is AES-GCM. +Note: If you want to use "Plugins and their settings", you have to enable this. + ### Passphrase The passphrase to used as the key of encryption. Please use the long text. @@ -195,6 +206,29 @@ You can set synchronization method at once as these pattern: - Sync on File Open : disabled - Sync on Start : disabled +### Use history +If you enable this option, you can keep document histories in your database. +(Not all intermediate changes are synchronized.) +You can check the changes caused by your edit and/or replication. + +### Enable plugin synchronization +If you want to use this feature, you have to activate this feature by this switch. + +### Sweep plugins automatically +Plugin sweep will run before replication automatically. + +### Sweep plugins periodically +Plugin sweep will run each 1 minute. + +### Notify updates +When replication is complete, a message will be notified if a newer version of the plugin applied to this device is configured on another device. + +### Device and Vault name +To save the plugins, you have to set a unique name every each device. + +### Open +Open the "Plugins and their settings" dialog. + ## Hatch From here, everything is under the hood. Please handle it with care. diff --git a/manifest.json b/manifest.json index f6d3be0..7994fa4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.7.2", + "version": "0.8.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 4255d2a..f544945 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.7.2", + "version": "0.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.7.2", + "version": "0.8.0", "license": "MIT", "dependencies": { "diff-match-patch": "^1.0.5", @@ -27,7 +27,7 @@ "eslint": "^7.32.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-import": "^2.25.2", - "obsidian": "^0.13.11", + "obsidian": "^0.13.30", "rollup": "^2.32.1", "svelte-preprocess": "^4.10.2", "tslib": "^2.2.0", @@ -2659,9 +2659,9 @@ } }, "node_modules/obsidian": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.11.tgz", - "integrity": "sha512-KxOvAh4CG5vzcukmHvyuK9hUIr6ZFlM9FQfGZEwrrEV8VG2/W2Tk5cWrg0VM7EkGE3QBmjX6owjIDIO8QDXVUQ==", + "version": "0.13.30", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.30.tgz", + "integrity": "sha512-uAOrIyeHE9qYzg1Qjfpy/qlyLUFX9oyKWeHYO8NVDoI+pm5VUTMe7XWcsXPwb9iVsVmggVJcdV15Vqm9bljhxQ==", "dev": true, "dependencies": { "@codemirror/state": "^0.19.6", @@ -5470,9 +5470,9 @@ } }, "obsidian": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.11.tgz", - "integrity": "sha512-KxOvAh4CG5vzcukmHvyuK9hUIr6ZFlM9FQfGZEwrrEV8VG2/W2Tk5cWrg0VM7EkGE3QBmjX6owjIDIO8QDXVUQ==", + "version": "0.13.30", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.30.tgz", + "integrity": "sha512-uAOrIyeHE9qYzg1Qjfpy/qlyLUFX9oyKWeHYO8NVDoI+pm5VUTMe7XWcsXPwb9iVsVmggVJcdV15Vqm9bljhxQ==", "dev": true, "requires": { "@codemirror/state": "^0.19.6", diff --git a/package.json b/package.json index e0082d6..8e6920e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.7.2", + "version": "0.8.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", @@ -24,7 +24,7 @@ "eslint": "^7.32.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-import": "^2.25.2", - "obsidian": "^0.13.11", + "obsidian": "^0.13.30", "rollup": "^2.32.1", "tslib": "^2.2.0", "typescript": "^4.2.4", diff --git a/src/DocumentHistoryModal.ts b/src/DocumentHistoryModal.ts index 4508c21..80eec31 100644 --- a/src/DocumentHistoryModal.ts +++ b/src/DocumentHistoryModal.ts @@ -2,6 +2,8 @@ import { TFile, Modal, App } from "obsidian"; import { path2id, escapeStringToHTML } from "./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"; export class DocumentHistoryModal extends Modal { plugin: ObsidianLiveSyncPlugin; @@ -14,6 +16,7 @@ export class DocumentHistoryModal extends Modal { file: string; revs_info: PouchDB.Core.RevisionInfo[] = []; + currentText = ""; constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile) { super(app); @@ -37,6 +40,7 @@ export class DocumentHistoryModal extends Modal { const index = this.revs_info.length - 1 - (this.range.value as any) / 1; const rev = this.revs_info[index]; const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false); + this.currentText = ""; if (w === false) { this.info.innerHTML = ""; @@ -44,6 +48,7 @@ export class DocumentHistoryModal extends Modal { } else { this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`; let result = ""; + this.currentText = w.data; if (this.showDiff) { const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1); if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) { @@ -124,6 +129,14 @@ export class DocumentHistoryModal extends Modal { this.contentView = div; div.addClass("op-scrollable"); div.addClass("op-pre"); + const buttons = contentEl.createDiv(""); + buttons.createEl("button", { text: "Copy to clipboard" }, (e) => { + e.addClass("mod-cta"); + e.addEventListener("click", async () => { + await navigator.clipboard.writeText(this.currentText); + Logger(`Old content copied to clipboard`, LOG_LEVEL.NOTICE); + }); + }); } onClose() { const { contentEl } = this; diff --git a/src/LocalPouchDB.ts b/src/LocalPouchDB.ts index 28e9b35..21a87e6 100644 --- a/src/LocalPouchDB.ts +++ b/src/LocalPouchDB.ts @@ -743,7 +743,7 @@ export class LocalPouchDB { username: setting.couchDB_USER, password: setting.couchDB_PASSWORD, }; - const dbret = await connectRemoteCouchDB(uri, auth); + 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(); @@ -820,7 +820,7 @@ export class LocalPouchDB { Logger("Another replication running."); return false; } - const dbret = await connectRemoteCouchDB(uri, auth); + const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI); if (typeof dbret === "string") { Logger(`could not connect to ${uri}: ${dbret}`, LOG_LEVEL.NOTICE); return false; @@ -1081,7 +1081,7 @@ export class LocalPouchDB { username: setting.couchDB_USER, password: setting.couchDB_PASSWORD, }; - const con = await connectRemoteCouchDB(uri, auth); + const con = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI); if (typeof con == "string") return; try { await con.db.destroy(); @@ -1099,7 +1099,7 @@ export class LocalPouchDB { username: setting.couchDB_USER, password: setting.couchDB_PASSWORD, }; - const con2 = await connectRemoteCouchDB(uri, auth); + const con2 = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI); if (typeof con2 === "string") return; Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE); } @@ -1109,7 +1109,7 @@ export class LocalPouchDB { username: setting.couchDB_USER, password: setting.couchDB_PASSWORD, }; - const dbret = await connectRemoteCouchDB(uri, auth); + const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI); if (typeof dbret === "string") { Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); return; @@ -1143,7 +1143,7 @@ export class LocalPouchDB { username: setting.couchDB_USER, password: setting.couchDB_PASSWORD, }; - const dbret = await connectRemoteCouchDB(uri, auth); + const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI); if (typeof dbret === "string") { Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); return; diff --git a/src/ObsidianLiveSyncSettingTab.ts b/src/ObsidianLiveSyncSettingTab.ts index f4dd4e8..7a60d3a 100644 --- a/src/ObsidianLiveSyncSettingTab.ts +++ b/src/ObsidianLiveSyncSettingTab.ts @@ -14,10 +14,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.plugin = plugin; } async testConnection(): Promise { - const db = await connectRemoteCouchDB(this.plugin.settings.couchDB_URI + (this.plugin.settings.couchDB_DBNAME == "" ? "" : "/" + this.plugin.settings.couchDB_DBNAME), { - username: this.plugin.settings.couchDB_USER, - password: this.plugin.settings.couchDB_PASSWORD, - }); + const db = await connectRemoteCouchDB( + this.plugin.settings.couchDB_URI + (this.plugin.settings.couchDB_DBNAME == "" ? "" : "/" + this.plugin.settings.couchDB_DBNAME), + { + username: this.plugin.settings.couchDB_USER, + password: this.plugin.settings.couchDB_PASSWORD, + }, + this.plugin.settings.disableRequestURI + ); if (typeof db === "string") { this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE); return; @@ -165,6 +169,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.plugin.settings.couchDB_DBNAME = value; 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(); + }) ) ); @@ -603,7 +613,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { ); new Setting(containerMiscellaneousEl) - .setName("Use history (beta)") + .setName("Use history") .setDesc("Use history dialog (Restart required, auto compaction would be disabled, and more storage will be consumed)") .addToggle((toggle) => toggle.setValue(this.plugin.settings.useHistory).onChange(async (value) => { @@ -832,12 +842,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }) ); - new Setting(containerPluginSettings).setName("Show own plugins and settings").addToggle((toggle) => - toggle.setValue(this.plugin.settings.showOwnPlugins).onChange(async (value) => { - this.plugin.settings.showOwnPlugins = value; - await this.plugin.saveSettings(); - }) - ); new Setting(containerPluginSettings) .setName("Sweep plugins automatically") diff --git a/src/types.ts b/src/types.ts index 5c42751..804711c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,6 +60,7 @@ export interface ObsidianLiveSyncSettings { batch_size: number; batches_limit: number; useHistory: boolean; + disableRequestURI: boolean; } export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = { @@ -101,6 +102,7 @@ export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = { batch_size: 250, batches_limit: 40, useHistory: false, + disableRequestURI: false, }; export const PERIODIC_PLUGIN_SWEEP = 60; diff --git a/src/utils_couchdb.ts b/src/utils_couchdb.ts index 5465963..d2d6856 100644 --- a/src/utils_couchdb.ts +++ b/src/utils_couchdb.ts @@ -2,6 +2,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 { requestUrl, RequestUrlParam } from "obsidian"; export const isValidRemoteCouchDBURI = (uri: string): boolean => { if (uri.startsWith("https://")) return true; @@ -12,8 +13,17 @@ let last_post_successed = false; export const getLastPostFailedBySize = () => { return !last_post_successed; }; -export const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }): Promise; info: PouchDB.Core.DatabaseInfo }> => { + +export const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }, disableRequestURI: boolean): Promise; info: PouchDB.Core.DatabaseInfo }> => { if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid"; + let authHeader = ""; + if (auth.username && auth.password) { + const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`)); + const encoded = window.btoa(utf8str); + authHeader = "Basic " + encoded; + } else { + authHeader = ""; + } const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = { adapter: "http", auth, @@ -35,6 +45,54 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string } size = ` (${opts_length})`; } + + if (!disableRequestURI && typeof url == "string" && typeof (opts.body ?? "") == "string") { + const body = opts.body as string; + + const transformedHeaders = { ...(opts.headers as Record) }; + if (authHeader != "") transformedHeaders["authorization"] = authHeader; + delete transformedHeaders["host"]; + delete transformedHeaders["Host"]; + delete transformedHeaders["content-length"]; + delete transformedHeaders["Content-Length"]; + const requestParam: RequestUrlParam = { + url: url as string, + method: opts.method, + body: body, + headers: transformedHeaders, + contentType: "application/json", + // contentType: opts.headers, + }; + + try { + const r = await requestUrl(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, { + headers: r.headers, + status: r.status, + statusText: `${r.status}`, + }); + } catch (ex) { + Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE); + if (!size_ok && (method == "POST" || method == "PUT")) { + last_post_successed = false; + } + Logger(ex); + throw ex; + } + } + + // -old implementation + try { const responce: Response = await fetch(url, opts); if (method == "POST" || method == "PUT") {