mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-13 17:55:56 +00:00
Implemented:
- Exporting settings and setup from uri. Fixed: - Change "Leaf" into "Chunk" - Reduced meaninglessly verbose logging - Trimmed deadcode.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.10.1",
|
"version": "0.11.0",
|
||||||
"minAppVersion": "0.9.12",
|
"minAppVersion": "0.9.12",
|
||||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"author": "vorotamoroz",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.10.1",
|
"version": "0.11.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.10.1",
|
"version": "0.11.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"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.",
|
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
MAX_DOC_SIZE,
|
MAX_DOC_SIZE,
|
||||||
MAX_DOC_SIZE_BIN,
|
MAX_DOC_SIZE_BIN,
|
||||||
NODEINFO_DOCID,
|
NODEINFO_DOCID,
|
||||||
RECENT_MOFIDIED_DOCS_QTY,
|
|
||||||
VER,
|
VER,
|
||||||
MILSTONE_DOCID,
|
MILSTONE_DOCID,
|
||||||
DatabaseConnectingStatus,
|
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() {
|
async isOldDatabaseExists() {
|
||||||
const db = new PouchDB<EntryDoc>(this.dbname + "-livesync", {
|
const db = new PouchDB<EntryDoc>(this.dbname + "-livesync", {
|
||||||
auto_compaction: this.settings.useHistory ? false : true,
|
auto_compaction: this.settings.useHistory ? false : true,
|
||||||
@@ -305,7 +291,7 @@ export class LocalPouchDB {
|
|||||||
waitForLeafReady(id: string): Promise<boolean> {
|
waitForLeafReady(id: string): Promise<boolean> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
// Set timeout.
|
// 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") {
|
if (typeof this.leafArrivedCallbacks[id] == "undefined") {
|
||||||
this.leafArrivedCallbacks[id] = [];
|
this.leafArrivedCallbacks[id] = [];
|
||||||
}
|
}
|
||||||
@@ -329,21 +315,21 @@ export class LocalPouchDB {
|
|||||||
this.hashCaches.set(id, w.data);
|
this.hashCaches.set(id, w.data);
|
||||||
return w.data;
|
return w.data;
|
||||||
}
|
}
|
||||||
throw new Error(`retrive leaf, but it was not leaf.`);
|
throw new Error(`Corrupted chunk detected.`);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (ex.status && ex.status == 404) {
|
if (ex.status && ex.status == 404) {
|
||||||
if (waitForReady) {
|
if (waitForReady) {
|
||||||
// just leaf is not ready.
|
// just leaf is not ready.
|
||||||
// wait for on
|
// wait for on
|
||||||
if ((await this.waitForLeafReady(id)) === false) {
|
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);
|
return this.getDBLeaf(id, false);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Leaf was not found");
|
throw new Error("Chunk was not found");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger(`Something went wrong on retriving leaf`);
|
Logger(`Something went wrong on retriving chunk`);
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,11 +433,11 @@ export class LocalPouchDB {
|
|||||||
try {
|
try {
|
||||||
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady)));
|
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady)));
|
||||||
if (dump) {
|
if (dump) {
|
||||||
Logger(`childrens:`);
|
Logger(`Chunks:`);
|
||||||
Logger(childrens);
|
Logger(childrens);
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} 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);
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
this.corruptedEntries[obj._id] = obj;
|
this.corruptedEntries[obj._id] = obj;
|
||||||
return false;
|
return false;
|
||||||
@@ -515,7 +501,7 @@ export class LocalPouchDB {
|
|||||||
if (!obj.type || (obj.type && obj.type == "notes")) {
|
if (!obj.type || (obj.type && obj.type == "notes")) {
|
||||||
obj._deleted = true;
|
obj._deleted = true;
|
||||||
const r = await this.localDatabase.put(obj);
|
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") {
|
if (typeof this.corruptedEntries[obj._id] != "undefined") {
|
||||||
delete this.corruptedEntries[obj._id];
|
delete this.corruptedEntries[obj._id];
|
||||||
}
|
}
|
||||||
@@ -526,7 +512,6 @@ export class LocalPouchDB {
|
|||||||
obj._deleted = true;
|
obj._deleted = true;
|
||||||
const r = await this.localDatabase.put(obj);
|
const r = await this.localDatabase.put(obj);
|
||||||
Logger(`entry removed:${obj._id}-${r.rev}`);
|
Logger(`entry removed:${obj._id}-${r.rev}`);
|
||||||
this.updateRecentModifiedDocs(r.id, r.rev, true);
|
|
||||||
if (typeof this.corruptedEntries[obj._id] != "undefined") {
|
if (typeof this.corruptedEntries[obj._id] != "undefined") {
|
||||||
delete this.corruptedEntries[obj._id];
|
delete this.corruptedEntries[obj._id];
|
||||||
}
|
}
|
||||||
@@ -579,7 +564,6 @@ export class LocalPouchDB {
|
|||||||
const item = await this.localDatabase.get(v);
|
const item = await this.localDatabase.get(v);
|
||||||
item._deleted = true;
|
item._deleted = true;
|
||||||
await this.localDatabase.put(item);
|
await this.localDatabase.put(item);
|
||||||
this.updateRecentModifiedDocs(item._id, item._rev, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
deleteCount++;
|
deleteCount++;
|
||||||
@@ -702,21 +686,21 @@ export class LocalPouchDB {
|
|||||||
try {
|
try {
|
||||||
const result = await this.localDatabase.bulkDocs(newLeafs);
|
const result = await this.localDatabase.bulkDocs(newLeafs);
|
||||||
for (const item of result) {
|
for (const item of result) {
|
||||||
if ((item as any).ok) {
|
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).status && (item as any).status == 409) {
|
if ((item as any).status && (item as any).status == 409) {
|
||||||
// conflicted, but it would be ok in childrens.
|
// conflicted, but it would be ok in childrens.
|
||||||
} else {
|
} 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);
|
Logger(item);
|
||||||
saved = false;
|
saved = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (saved) {
|
||||||
|
Logger(`Chunk saved:${newLeafs.length} chunks`);
|
||||||
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger("ERROR ON SAVING LEAVES:", LOG_LEVEL.NOTICE);
|
Logger("Chunk save failed:", LOG_LEVEL.NOTICE);
|
||||||
Logger(ex, LOG_LEVEL.NOTICE);
|
Logger(ex, LOG_LEVEL.NOTICE);
|
||||||
saved = false;
|
saved = false;
|
||||||
}
|
}
|
||||||
@@ -748,7 +732,6 @@ export class LocalPouchDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const r = await this.localDatabase.put(newDoc, { force: true });
|
const r = await this.localDatabase.put(newDoc, { force: true });
|
||||||
this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted);
|
|
||||||
if (typeof this.corruptedEntries[note._id] != "undefined") {
|
if (typeof this.corruptedEntries[note._id] != "undefined") {
|
||||||
delete this.corruptedEntries[note._id];
|
delete this.corruptedEntries[note._id];
|
||||||
}
|
}
|
||||||
|
|||||||
119
src/main.ts
119
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 { 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";
|
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 PluginPane from "./PluginPane.svelte";
|
||||||
import { id2path, path2id } from "./utils";
|
import { id2path, path2id } from "./utils";
|
||||||
|
import { decrypt, encrypt } from "./lib/src/e2ee";
|
||||||
const isDebug = false;
|
const isDebug = false;
|
||||||
setNoticeClass(Notice);
|
setNoticeClass(Notice);
|
||||||
class PluginDialogModal extends Modal {
|
class PluginDialogModal extends Modal {
|
||||||
@@ -57,6 +58,45 @@ class PluginDialogModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class PopoverYesNo extends FuzzySuggestModal<string> {
|
||||||
|
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 {
|
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||||
settings: ObsidianLiveSyncSettings;
|
settings: ObsidianLiveSyncSettings;
|
||||||
@@ -200,6 +240,81 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
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({
|
this.addCommand({
|
||||||
id: "livesync-replicate",
|
id: "livesync-replicate",
|
||||||
name: "Replicate now",
|
name: "Replicate now",
|
||||||
@@ -587,8 +702,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
//--> Basic document Functions
|
//--> Basic document Functions
|
||||||
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
|
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
|
||||||
|
|
||||||
// eslint-disable-next-line require-await
|
|
||||||
lastLog = "";
|
lastLog = "";
|
||||||
|
// eslint-disable-next-line require-await
|
||||||
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
|
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
|
||||||
if (level == LOG_LEVEL.DEBUG && !isDebug) {
|
if (level == LOG_LEVEL.DEBUG && !isDebug) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user