fixed some sync, merging errors and speed up.

This commit is contained in:
vrtmrz
2021-10-18 15:07:44 +09:00
parent 518ae46cf9
commit 531fb97cd9
4 changed files with 357 additions and 241 deletions

View File

@@ -7,7 +7,6 @@ Runs in Mac, Android, Windows, and iOS.
![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif) ![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
**It's beta. Please make sure back your vault up!** **It's beta. Please make sure back your vault up!**
Limitations: File deletion handling is not completed. Limitations: File deletion handling is not completed.
@@ -18,15 +17,30 @@ Limitations: File deletion handling is not completed.
- Self-Hosted data synchronization with conflict detection and resolving in Obsidian. - Self-Hosted data synchronization with conflict detection and resolving in Obsidian.
- Off line sync is also available. - Off line sync is also available.
## How to use the beta build ## How to use
1. download this repo and expand `[your-vault]/.obsidian/plugins/` (PC, Mac and Android will work) 1. Install from Obsidian, or clone this repo and run `npm run build` ,copy `main.js`, `styles.css` and `manifest.json` into `[your-vault]/.obsidian/plugins/` (PC, Mac and Android will work)
1. enable obsidian livesync in the settings dialog. 2. Enable obsidian livesync in the settings dialog.
1. If you use your self-hosted CouchDB, set your server's info. 3. If you use your self-hosted CouchDB, set your server's info.
1. or Use [IBM Cloudant](https://www.ibm.com/cloud/cloudant), take an account and enable **Cloudant** in [Catalog](https://cloud.ibm.com/catalog#services) 4. or Use [IBM Cloudant](https://www.ibm.com/cloud/cloudant), take an account and enable **Cloudant** in [Catalog](https://cloud.ibm.com/catalog#services)
Note please choose "IAM and legacy credentials" for the Authentication method Note please choose "IAM and legacy credentials" for the Authentication method
Setup details are in Couldant Setup Section. Setup details are in Couldant Setup Section.
1. Setup LiveSync or SyncOnSave or SyncOnStart as you like. 5. Setup LiveSync or SyncOnSave or SyncOnStart as you like.
## When your database looks corrupted
obsidian-livesync changes data treatment of markdown files since 0.1.0
When you are troubled with synchronization, **Please reset local and remote databases**.
*Note: Without synchronization, your files won't be deleted.*
1. Disable any synchronizations on all devices.
2. From the most reliable device<sup>(_The device_)</sup>, back your vault up.
3. Click "Reset local database" on all devices.
4. From _The device_ click "Reset remote database".
5. From _The device_ click "Init Database again".
6. Enable any sync or Hit Replication button.
And wait for a minute. your data will be uploaded and synchronized with all devices again.
## Cloudant Setup ## Cloudant Setup
@@ -107,4 +121,5 @@ example values.
| CouchDB Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 | | CouchDB Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 |
# License # License
The source code is licensed MIT. The source code is licensed MIT.

409
main.ts
View File

@@ -1,4 +1,4 @@
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, ItemView } from "obsidian"; import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder } from "obsidian";
import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser"; import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import xxhash from "xxhash-wasm"; import xxhash from "xxhash-wasm";
@@ -8,7 +8,17 @@ import xxhash from "xxhash-wasm";
// const MAX_DOC_SIZE = 921600; // const MAX_DOC_SIZE = 921600;
const MAX_DOC_SIZE = 1000; // for .md file, but if delimiters exists. use that before. const MAX_DOC_SIZE = 1000; // for .md file, but if delimiters exists. use that before.
const MAX_DOC_SIZE_BIN = 102400; // 100kb const MAX_DOC_SIZE_BIN = 102400; // 100kb
const VER = 10 const VER = 10;
const RECENT_MOFIDIED_DOCS_QTY = 30;
const LOG_LEVEL = {
VERBOSE: 1,
INFO: 10,
NOTICE: 100,
URGENT: 1000,
} as const;
type LOG_LEVEL = typeof LOG_LEVEL[keyof typeof LOG_LEVEL];
interface ObsidianLiveSyncSettings { interface ObsidianLiveSyncSettings {
couchDB_URI: string; couchDB_URI: string;
@@ -22,7 +32,8 @@ interface ObsidianLiveSyncSettings {
gcDelay: number; gcDelay: number;
versionUpFlash: string; versionUpFlash: string;
minimumChunkSize: number; minimumChunkSize: number;
longLineThreshold: number longLineThreshold: number;
showVerboseLog: boolean;
} }
const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = { const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
@@ -38,8 +49,8 @@ const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
versionUpFlash: "", versionUpFlash: "",
minimumChunkSize: 20, minimumChunkSize: 20,
longLineThreshold: 250, longLineThreshold: 250,
showVerboseLog: false,
}; };
interface Entry { interface Entry {
_id: string; _id: string;
data: string; data: string;
@@ -48,6 +59,7 @@ interface Entry {
mtime: number; mtime: number;
size: number; size: number;
_deleted?: boolean; _deleted?: boolean;
_conflicts?: string[];
type?: "notes"; type?: "notes";
} }
interface NewEntry { interface NewEntry {
@@ -58,6 +70,7 @@ interface NewEntry {
mtime: number; mtime: number;
size: number; size: number;
_deleted?: boolean; _deleted?: boolean;
_conflicts?: string[];
NewNote: true; NewNote: true;
type: "newnote"; type: "newnote";
} }
@@ -70,11 +83,12 @@ interface PlainEntry {
size: number; size: number;
_deleted?: boolean; _deleted?: boolean;
NewNote: true; NewNote: true;
_conflicts?: string[];
type: "plain"; type: "plain";
} }
type LoadedEntry = Entry & { type LoadedEntry = Entry & {
children: string[]; children: string[];
datatype: "plain" | "newnote" datatype: "plain" | "newnote";
}; };
interface EntryLeaf { interface EntryLeaf {
@@ -106,6 +120,8 @@ type Credential = {
password: string; password: string;
}; };
type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;
//-->Functions. //-->Functions.
function arrayBufferToBase64(buffer: ArrayBuffer) { function arrayBufferToBase64(buffer: ArrayBuffer) {
var binary = ""; var binary = "";
@@ -183,12 +199,19 @@ class LocalPouchDB {
plugin: ObsidianLiveSyncPlugin; plugin: ObsidianLiveSyncPlugin;
auth: Credential; auth: Credential;
dbname: string; dbname: string;
addLog: (message: any, isNotify?: boolean) => Promise<void>; addLog: (message: any, levlel?: LOG_LEVEL) => Promise<void>;
localDatabase: PouchDB.Database<EntryDoc>; localDatabase: PouchDB.Database<EntryDoc>;
recentModifiedDocs: string[] = []; recentModifiedDocs: string[] = [];
h32: (input: string, seed?: number) => string; h32: (input: string, seed?: number) => string;
h64: (input: string, seedHigh?: number, seedLow?: number) => string; h64: (input: string, seedHigh?: number, seedLow?: number) => string;
hashCache: {
[key: string]: string;
} = {};
hashCacheRev: {
[key: string]: string;
} = {};
constructor(app: App, plugin: ObsidianLiveSyncPlugin, dbname: string) { constructor(app: App, plugin: ObsidianLiveSyncPlugin, dbname: string) {
this.plugin = plugin; this.plugin = plugin;
this.app = app; this.app = app;
@@ -210,11 +233,18 @@ class LocalPouchDB {
} }
return "disabled"; return "disabled";
} }
updateRecentModifiedDocs(id: string, rev: string) { disposeHashCache() {
this.hashCache = {};
this.hashCacheRev = {};
}
updateRecentModifiedDocs(id: string, rev: string, deleted: boolean) {
let idrev = id + rev; let idrev = id + rev;
if (deleted) {
this.recentModifiedDocs = this.recentModifiedDocs.filter((e) => e != idrev);
} else {
this.recentModifiedDocs.push(idrev); this.recentModifiedDocs.push(idrev);
if (this.recentModifiedDocs.length > 10) { this.recentModifiedDocs = this.recentModifiedDocs.slice(0 - RECENT_MOFIDIED_DOCS_QTY);
this.recentModifiedDocs = this.recentModifiedDocs.slice(-30);
} }
} }
isSelfModified(id: string, rev: string): boolean { isSelfModified(id: string, rev: string): boolean {
@@ -229,17 +259,17 @@ class LocalPouchDB {
revs_limit: 100, revs_limit: 100,
deterministic_revs: true, deterministic_revs: true,
}); });
await this.prepareHashArg(); await this.prepareHashFunctions();
} }
async prepareHashArg() { async prepareHashFunctions() {
if (this.h32 != null) return; if (this.h32 != null) return;
const { h32, h64 } = await xxhash(); const { h32, h64 } = await xxhash();
this.h32 = h32; this.h32 = h32;
this.h64 = h64; this.h64 = h64;
} }
async getDatabaseDoc(id: string, opt?: any): Promise<false | LoadedEntry> { async getDBEntry(id: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> {
try { try {
let obj: EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = null; let obj: EntryDocResponse = null;
if (opt) { if (opt) {
obj = await this.localDatabase.get(id, opt); obj = await this.localDatabase.get(id, opt);
} else { } else {
@@ -254,7 +284,7 @@ class LocalPouchDB {
//Check it out and fix docs to regular case //Check it out and fix docs to regular case
if (!obj.type || (obj.type && obj.type == "notes")) { if (!obj.type || (obj.type && obj.type == "notes")) {
let note = obj as Entry; let note = obj as Entry;
let doc: LoadedEntry = { let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = {
data: note.data, data: note.data,
_id: note._id, _id: note._id,
ctime: note.ctime, ctime: note.ctime,
@@ -262,6 +292,7 @@ class LocalPouchDB {
size: note.size, size: note.size,
_deleted: obj._deleted, _deleted: obj._deleted,
_rev: obj._rev, _rev: obj._rev,
_conflicts: obj._conflicts,
children: [], children: [],
datatype: "newnote", datatype: "newnote",
}; };
@@ -273,6 +304,9 @@ class LocalPouchDB {
try { try {
let childrens = []; let childrens = [];
for (var v of obj.children) { for (var v of obj.children) {
if (typeof this.hashCacheRev[v] !== "undefined") {
childrens.push(this.hashCacheRev[v]);
} else {
try { try {
let elem = await this.localDatabase.get(v); let elem = await this.localDatabase.get(v);
if (elem.type && elem.type == "leaf") { if (elem.type && elem.type == "leaf") {
@@ -282,14 +316,15 @@ class LocalPouchDB {
} }
} catch (ex) { } catch (ex) {
if (ex.status && ex.status == 404) { if (ex.status && ex.status == 404) {
this.addLog(`Missing document content!, could not read ${v} of ${obj._id} from database.`, true); this.addLog(`Missing document content!, could not read ${v} of ${obj._id}(${obj._rev}) from database.`, LOG_LEVEL.NOTICE);
return false; return false;
} }
throw ex; throw ex;
} }
} }
}
let data = childrens.join(""); let data = childrens.join("");
let doc: LoadedEntry = { let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = {
data: data, data: data,
_id: obj._id, _id: obj._id,
ctime: obj.ctime, ctime: obj.ctime,
@@ -298,16 +333,17 @@ class LocalPouchDB {
_deleted: obj._deleted, _deleted: obj._deleted,
_rev: obj._rev, _rev: obj._rev,
children: obj.children, children: obj.children,
datatype: obj.type datatype: obj.type,
_conflicts: obj._conflicts,
}; };
return doc; return doc;
} catch (ex) { } catch (ex) {
if (ex.status && ex.status == 404) { if (ex.status && ex.status == 404) {
this.addLog(`Missing document content!, could not read ${obj._id} from database.`, true); this.addLog(`Missing document content!, could not read ${obj._id} from database.`, LOG_LEVEL.NOTICE);
return false; return false;
} }
this.addLog(`Something went wrong on reading ${obj._id} from database.`, true); this.addLog(`Something went wrong on reading ${obj._id} from database.`, LOG_LEVEL.NOTICE);
this.addLog(ex); this.addLog(ex);
} }
} }
@@ -319,9 +355,9 @@ class LocalPouchDB {
} }
return false; return false;
} }
async deleteDBEntry(id: string, opt?: any): Promise<boolean> { async deleteDBEntry(id: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> {
try { try {
let obj: EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = null; let obj: EntryDocResponse = null;
if (opt) { if (opt) {
obj = await this.localDatabase.get(id, opt); obj = await this.localDatabase.get(id, opt);
} else { } else {
@@ -336,15 +372,15 @@ class LocalPouchDB {
if (!obj.type || (obj.type && obj.type == "notes")) { if (!obj.type || (obj.type && obj.type == "notes")) {
obj._deleted = true; obj._deleted = true;
let r = await this.localDatabase.put(obj); let r = await this.localDatabase.put(obj);
this.updateRecentModifiedDocs(r.id, r.rev); this.updateRecentModifiedDocs(r.id, r.rev, true);
return true; return true;
// simple note // simple note
} }
if (obj.type == "newnote" || obj.type == "plain") { if (obj.type == "newnote" || obj.type == "plain") {
obj._deleted = true; obj._deleted = true;
let r = await this.localDatabase.put(obj); let r = await this.localDatabase.put(obj);
this.addLog(`entry removed:${obj._id}`); this.addLog(`entry removed:${obj._id}-${r.rev}`);
this.updateRecentModifiedDocs(r.id, r.rev); this.updateRecentModifiedDocs(r.id, r.rev, true);
return true; return true;
} }
} catch (ex) { } catch (ex) {
@@ -363,6 +399,7 @@ class LocalPouchDB {
let skiped = 0; let skiped = 0;
let pieceSize = MAX_DOC_SIZE_BIN; let pieceSize = MAX_DOC_SIZE_BIN;
let plainSplit = false; let plainSplit = false;
let cacheUsed = 0;
if (note._id.endsWith(".md")) { if (note._id.endsWith(".md")) {
pieceSize = MAX_DOC_SIZE; pieceSize = MAX_DOC_SIZE;
plainSplit = true; plainSplit = true;
@@ -375,7 +412,7 @@ class LocalPouchDB {
// 2. \n\n shold break // 2. \n\n shold break
// 3. \r\n\r\n should break // 3. \r\n\r\n should break
// 4. \n# should break. // 4. \n# should break.
let cPieceSize = pieceSize let cPieceSize = pieceSize;
let minimumChunkSize = this.plugin.settings.minimumChunkSize; let minimumChunkSize = this.plugin.settings.minimumChunkSize;
if (minimumChunkSize < 10) minimumChunkSize = 10; if (minimumChunkSize < 10) minimumChunkSize = 10;
let longLineThreshold = this.plugin.settings.longLineThreshold; let longLineThreshold = this.plugin.settings.longLineThreshold;
@@ -406,48 +443,49 @@ class LocalPouchDB {
if (n4 > 0 && cPieceSize < n4) cPieceSize = n4 + 0; if (n4 > 0 && cPieceSize < n4) cPieceSize = n4 + 0;
cPieceSize++; cPieceSize++;
} }
} while (cPieceSize < minimumChunkSize) } while (cPieceSize < minimumChunkSize);
// console.log("and we use:" + cPieceSize)
} }
let piece = leftData.substring(0, cPieceSize); let piece = leftData.substring(0, cPieceSize);
// if (plainSplit) {
// this.addLog(`piece_len:${cPieceSize}`);
// this.addLog("piece:" + piece);
// }
leftData = leftData.substring(cPieceSize); leftData = leftData.substring(cPieceSize);
processed++; processed++;
let leafid = "";
// Get has of piece. // Get has of piece.
let hashedPiece = this.h32(piece); let hashedPiece: string = "";
let leafid = "h:" + hashedPiece;
let hashQ: number = 0; // if hash collided, **IF**, count it up. let hashQ: number = 0; // if hash collided, **IF**, count it up.
let tryNextHash = false; let tryNextHash = false;
let needMake = true; let needMake = true;
if (typeof this.hashCache[piece] !== "undefined") {
hashedPiece = "";
leafid = this.hashCache[piece];
needMake = false;
skiped++;
cacheUsed++;
} else {
hashedPiece = this.h32(piece);
leafid = "h:" + hashedPiece;
do { do {
let nleafid = leafid; let nleafid = leafid;
try { try {
nleafid = `${leafid}${hashQ}`; nleafid = `${leafid}${hashQ}`;
// console.log(nleafid);
let pieceData = await this.localDatabase.get<EntryLeaf>(nleafid); let pieceData = await this.localDatabase.get<EntryLeaf>(nleafid);
if (pieceData.type == "leaf" && pieceData.data == piece) { if (pieceData.type == "leaf" && pieceData.data == piece) {
// this.addLog("hash:data exists.");
leafid = nleafid; leafid = nleafid;
needMake = false; needMake = false;
tryNextHash = false; tryNextHash = false;
this.hashCache[piece] = leafid;
this.hashCacheRev[leafid] = piece;
} else if (pieceData.type == "leaf") { } else if (pieceData.type == "leaf") {
this.addLog("hash:collision!!"); this.addLog("hash:collision!!");
hashQ++; hashQ++;
tryNextHash = true; tryNextHash = true;
} else { } else {
// this.addLog("hash:no collision, it's not leaf. what's going on..");
leafid = nleafid; leafid = nleafid;
tryNextHash = false; tryNextHash = false;
} }
} catch (ex) { } catch (ex) {
if (ex.status && ex.status == 404) { if (ex.status && ex.status == 404) {
//not found, we can use it. //not found, we can use it.
// this.addLog(`hash:not found.`);
leafid = nleafid; leafid = nleafid;
needMake = true; needMake = true;
} else { } else {
@@ -464,9 +502,11 @@ class LocalPouchDB {
type: "leaf", type: "leaf",
}; };
let result = await this.localDatabase.put(d); let result = await this.localDatabase.put(d);
this.updateRecentModifiedDocs(result.id, result.rev); this.updateRecentModifiedDocs(result.id, result.rev, d._deleted);
if (result.ok) { if (result.ok) {
this.addLog(`ok:saven`); this.addLog(`save ok:id:${result.id} rev:${result.rev}`, LOG_LEVEL.VERBOSE);
this.hashCache[piece] = leafid;
this.hashCacheRev[leafid] = piece;
made++; made++;
} else { } else {
this.addLog("save faild"); this.addLog("save faild");
@@ -474,9 +514,11 @@ class LocalPouchDB {
} else { } else {
skiped++; skiped++;
} }
}
savenNotes.push(leafid); savenNotes.push(leafid);
} while (leftData != ""); } while (leftData != "");
this.addLog(`note content saven, pieces:${processed} new:${made}, skip:${skiped}`); this.addLog(`note content saven, pieces:${processed} new:${made}, skip:${skiped}, cache:${cacheUsed}`);
let newDoc: PlainEntry | NewEntry = { let newDoc: PlainEntry | NewEntry = {
NewNote: true, NewNote: true,
children: savenNotes, children: savenNotes,
@@ -487,7 +529,6 @@ class LocalPouchDB {
type: plainSplit ? "plain" : "newnote", type: plainSplit ? "plain" : "newnote",
}; };
let deldocs: string[] = [];
// Here for upsert logic, // Here for upsert logic,
try { try {
let old = await this.localDatabase.get(newDoc._id); let old = await this.localDatabase.get(newDoc._id);
@@ -503,8 +544,8 @@ class LocalPouchDB {
} }
} }
let r = await this.localDatabase.put(newDoc); let r = await this.localDatabase.put(newDoc);
this.updateRecentModifiedDocs(r.id, r.rev); this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted);
this.addLog(`note saven:${newDoc._id}`); this.addLog(`note saven:${newDoc._id}:${r.rev}`);
} }
syncHandler: PouchDB.Replication.Sync<{}> = null; syncHandler: PouchDB.Replication.Sync<{}> = null;
@@ -525,15 +566,19 @@ class LocalPouchDB {
} }
let dbret = await connectRemoteCouchDB(uri, auth); let dbret = await connectRemoteCouchDB(uri, auth);
if (dbret === false) { if (dbret === false) {
this.addLog(`could not connect to ${uri}`, true); this.addLog(`could not connect to ${uri}`, LOG_LEVEL.NOTICE);
return; return;
} }
let syncOption = keepAlive ? { live: true, retry: true } : {}; let syncOptionBase: PouchDB.Replication.SyncOptions = {
batch_size: 250,
batches_limit: 40,
};
let syncOption: PouchDB.Replication.SyncOptions = keepAlive ? { live: true, retry: true, heartbeat: 30000, ...syncOptionBase } : { ...syncOptionBase };
let db = dbret.db; let db = dbret.db;
//replicate once //replicate once
let replicate = this.localDatabase.replicate.from(db); let replicate = this.localDatabase.replicate.from(db, syncOptionBase);
// console.log("replication start.")
replicate replicate
.on("change", async (e) => { .on("change", async (e) => {
try { try {
@@ -547,7 +592,7 @@ class LocalPouchDB {
.on("complete", async (info) => { .on("complete", async (info) => {
replicate.removeAllListeners(); replicate.removeAllListeners();
replicate.cancel(); replicate.cancel();
// this.syncHandler = null; this.syncHandler = null;
if (this.syncHandler != null) { if (this.syncHandler != null) {
this.syncHandler.removeAllListeners(); this.syncHandler.removeAllListeners();
} }
@@ -566,26 +611,25 @@ class LocalPouchDB {
} }
}) })
.on("complete", (e) => { .on("complete", (e) => {
this.addLog("Replication completed", showResult); this.addLog("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
this.syncHandler = null; this.syncHandler = null;
}) })
.on("denied", (e) => { .on("denied", (e) => {
this.addLog("Replication denied", true); this.addLog("Replication denied", LOG_LEVEL.NOTICE);
// this.addLog(e); // this.addLog(e);
}) })
.on("error", (e) => { .on("error", (e) => {
this.addLog("Replication error", true); this.addLog("Replication error", LOG_LEVEL.NOTICE);
// this.addLog(e); // this.addLog(e);
}) })
.on("paused", (e) => { .on("paused", (e) => {
this.addLog("replication paused"); this.addLog("replication paused", LOG_LEVEL.VERBOSE);
// console.dir(this.syncHandler);
// this.addLog(e); // this.addLog(e);
}); });
// console.dir();
}) })
.on("error", () => { .on("error", (e) => {
this.addLog("Pulling Replication error", true); this.addLog("Pulling Replication error", LOG_LEVEL.NOTICE);
this.addLog(e);
}); });
} }
@@ -604,7 +648,7 @@ class LocalPouchDB {
await this.localDatabase.destroy(); await this.localDatabase.destroy();
this.localDatabase = null; this.localDatabase = null;
await this.initializeDatabase(); await this.initializeDatabase();
this.addLog("Local Database Reset", true); this.addLog("Local Database Reset", LOG_LEVEL.NOTICE);
} }
async tryResetRemoteDatabase(setting: ObsidianLiveSyncSettings) { async tryResetRemoteDatabase(setting: ObsidianLiveSyncSettings) {
await this.closeReplication(); await this.closeReplication();
@@ -618,10 +662,10 @@ class LocalPouchDB {
if (con === false) return; if (con === false) return;
try { try {
await con.db.destroy(); await con.db.destroy();
this.addLog("Remote Database Destroyed", true); this.addLog("Remote Database Destroyed", LOG_LEVEL.NOTICE);
await this.tryCreateRemoteDatabase(setting); await this.tryCreateRemoteDatabase(setting);
} catch (ex) { } catch (ex) {
this.addLog("something happend on Remote Database Destory", true); this.addLog("something happend on Remote Database Destory", LOG_LEVEL.NOTICE);
} }
} }
async tryCreateRemoteDatabase(setting: ObsidianLiveSyncSettings) { async tryCreateRemoteDatabase(setting: ObsidianLiveSyncSettings) {
@@ -633,7 +677,7 @@ class LocalPouchDB {
}; };
let con2 = await connectRemoteCouchDB(uri, auth); let con2 = await connectRemoteCouchDB(uri, auth);
if (con2 === false) return; if (con2 === false) return;
this.addLog("Remote Database Created or Connected", true); this.addLog("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
} }
async garbageCollect() { async garbageCollect() {
@@ -644,7 +688,7 @@ class LocalPouchDB {
let hashPieces: string[] = []; let hashPieces: string[] = [];
let usedPieces: string[] = []; let usedPieces: string[] = [];
do { do {
let result = await this.localDatabase.allDocs({ include_docs: true, skip: c, limit: 100 }); let result = await this.localDatabase.allDocs({ include_docs: true, skip: c, limit: 100, conflicts: true });
readCount = result.rows.length; readCount = result.rows.length;
if (readCount > 0) { if (readCount > 0) {
//there are some result //there are some result
@@ -653,6 +697,14 @@ class LocalPouchDB {
if (doc.type == "newnote" || doc.type == "plain") { if (doc.type == "newnote" || doc.type == "plain") {
// used pieces memo. // used pieces memo.
usedPieces = Array.from(new Set([...usedPieces, ...doc.children])); usedPieces = Array.from(new Set([...usedPieces, ...doc.children]));
if (doc._conflicts) {
for (let cid of doc._conflicts) {
let p = await this.localDatabase.get<EntryDoc>(doc._id, { rev: cid });
if (p.type == "newnote" || p.type == "plain") {
usedPieces = Array.from(new Set([...usedPieces, ...p.children]));
}
}
}
} }
if (doc.type == "leaf") { if (doc.type == "leaf") {
// all pieces. // all pieces.
@@ -743,6 +795,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false); this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false);
this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false); this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false);
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), delay, false); this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), delay, false);
this.registerWatchEvents(); this.registerWatchEvents();
this.parseReplicationResult = this.parseReplicationResult.bind(this); this.parseReplicationResult = this.parseReplicationResult.bind(this);
@@ -767,6 +820,35 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
}, 60 * 1000) }, 60 * 1000)
); );
this.addCommand({
id: "livesync-replicate",
name: "Replicate now",
callback: () => {
this.replicate();
},
});
this.addCommand({
id: "livesync-gc",
name: "garbage collect now",
callback: () => {
this.garbageCollect();
},
});
this.addCommand({
id: "livesync-toggle",
name: "Toggle LiveSync",
callback: () => {
if (this.settings.liveSync) {
this.settings.liveSync = false;
this.addLog("LiveSync Disabled.", LOG_LEVEL.NOTICE);
} else {
this.settings.liveSync = true;
this.addLog("LiveSync Enabled.", LOG_LEVEL.NOTICE);
}
this.realizeSettingSyncMode();
this.saveSettings();
},
});
this.watchWindowVisiblity = this.watchWindowVisiblity.bind(this); this.watchWindowVisiblity = this.watchWindowVisiblity.bind(this);
window.addEventListener("visibilitychange", this.watchWindowVisiblity); window.addEventListener("visibilitychange", this.watchWindowVisiblity);
} }
@@ -822,9 +904,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
watchWindowVisiblity() { watchWindowVisiblity() {
this.addLog("visiblity changed");
let isHidden = document.hidden; let isHidden = document.hidden;
// this.addLog(isHidden);
if (isHidden) { if (isHidden) {
this.localDatabase.closeReplication(); this.localDatabase.closeReplication();
} else { } else {
@@ -840,6 +920,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
watchWorkspaceOpen(file: TFile) { watchWorkspaceOpen(file: TFile) {
if (file == null) return; if (file == null) return;
this.localDatabase.disposeHashCache();
this.showIfConflicted(file); this.showIfConflicted(file);
this.gcHook(); this.gcHook();
} }
@@ -869,27 +950,26 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
//--> Basic document Functions //--> Basic document Functions
async addLog(message: any, isNotify?: boolean) { async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
// debugger; // debugger;
if (!isNotify && this.settings && this.settings.lessInformationInLog) { if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) {
return; return;
} }
// console.log(this.settings); if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL.VERBOSE) {
return;
}
let valutName = this.app.vault.getName();
let timestamp = new Date().toLocaleString(); let timestamp = new Date().toLocaleString();
let messagecontent = typeof message == "string" ? message : JSON.stringify(message, null, 2); let messagecontent = typeof message == "string" ? message : JSON.stringify(message, null, 2);
let newmessage = timestamp + "->" + messagecontent; let newmessage = timestamp + "->" + messagecontent;
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100); this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
// this.logMessage = [...this.logMessage, timestamp + ":" + newmessage].slice(-100); console.log(valutName + ":" + newmessage);
console.log(newmessage);
if (this.statusBar2 != null) { if (this.statusBar2 != null) {
this.statusBar2.setText(newmessage.substring(0, 60)); this.statusBar2.setText(newmessage.substring(0, 60));
} }
// if (this.onLogChanged != null) { if (level >= LOG_LEVEL.NOTICE) {
// this.onLogChanged();
// }
if (isNotify) {
new Notice(messagecontent); new Notice(messagecontent);
} }
} }
@@ -916,7 +996,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async doc2storage_create(docEntry: Entry, force?: boolean) { async doc2storage_create(docEntry: Entry, force?: boolean) {
let doc = await this.localDatabase.getDatabaseDoc(docEntry._id, { _rev: docEntry._rev }); let doc = await this.localDatabase.getDBEntry(docEntry._id, { rev: docEntry._rev });
if (doc === false) return; if (doc === false) return;
if (doc.datatype == "newnote") { if (doc.datatype == "newnote") {
let bin = base64ToArrayBuffer(doc.data); let bin = base64ToArrayBuffer(doc.data);
@@ -932,7 +1012,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.addLog("live : write to local (newfile:p) " + doc._id); this.addLog("live : write to local (newfile:p) " + doc._id);
await this.app.vault.trigger("create", newfile); await this.app.vault.trigger("create", newfile);
} else { } else {
this.addLog("live : New data imcoming, but we cound't parse that.1" + doc.datatype, true); this.addLog("live : New data imcoming, but we cound't parse that." + doc.datatype, LOG_LEVEL.NOTICE);
} }
} }
@@ -950,34 +1030,39 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (docEntry._deleted) { if (docEntry._deleted) {
//basically pass. //basically pass.
//but if there're no docs left, delete file. //but if there're no docs left, delete file.
let lastDocs = await this.localDatabase.getDatabaseDoc(docEntry._id); let lastDocs = await this.localDatabase.getDBEntry(docEntry._id);
if (lastDocs === false) { if (lastDocs === false) {
await this.deleteVaultItem(file); await this.deleteVaultItem(file);
} else { } else {
// it perhaps delete some revisions.
// may be we have to reload this
await this.pullFile(docEntry._id, null, true);
this.addLog(`delete skipped:${lastDocs._id}`); this.addLog(`delete skipped:${lastDocs._id}`);
} }
return; return;
} }
if (file.stat.mtime < docEntry.mtime || force) { if (file.stat.mtime < docEntry.mtime || force) {
let doc = await this.localDatabase.getDatabaseDoc(docEntry._id); let doc = await this.localDatabase.getDBEntry(docEntry._id);
let msg = "livesync : newer local files so write to local:" + file.path;
if (force) msg = "livesync : force write to local:" + file.path;
if (doc === false) return; if (doc === false) return;
// debugger;
if (doc.datatype == "newnote") { if (doc.datatype == "newnote") {
let bin = base64ToArrayBuffer(doc.data); let bin = base64ToArrayBuffer(doc.data);
if (bin != null) { if (bin != null) {
await this.ensureDirectory(doc._id);
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime }); await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
this.addLog("livesync : newer local files so write to local:" + file.path); this.addLog(msg);
await this.app.vault.trigger("modify", file); await this.app.vault.trigger("modify", file);
} }
} if (doc.datatype == "plain") { }
if (doc.datatype == "plain") {
await this.ensureDirectory(doc._id); await this.ensureDirectory(doc._id);
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime }); await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
this.addLog("livesync : newer local files so write to local:" + file.path); this.addLog(msg);
await this.app.vault.trigger("modify", file); await this.app.vault.trigger("modify", file);
} else { } else {
this.addLog("live : New data imcoming, but we cound't parse that.2:" + doc.datatype + "-", true); this.addLog("live : New data imcoming, but we cound't parse that.:" + doc.datatype + "-", LOG_LEVEL.NOTICE);
} }
} else if (file.stat.mtime > docEntry.mtime) { } else if (file.stat.mtime > docEntry.mtime) {
// newer local file. // newer local file.
@@ -987,7 +1072,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//eq.case //eq.case
} }
} }
async pouchdbChanged(change: Entry) { async handleDBChanged(change: Entry) {
let allfiles = this.app.vault.getFiles(); let allfiles = this.app.vault.getFiles();
let targetFiles = allfiles.filter((e) => e.path == change._id); let targetFiles = allfiles.filter((e) => e.path == change._id);
if (targetFiles.length == 0) { if (targetFiles.length == 0) {
@@ -1011,8 +1096,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.localDatabase.isSelfModified(change._id, change._rev)) { if (this.localDatabase.isSelfModified(change._id, change._rev)) {
return; return;
} }
this.addLog("replication change arrived"); this.addLog("replication change arrived", LOG_LEVEL.VERBOSE);
await this.pouchdbChanged(change); await this.handleDBChanged(change);
this.gcHook(); this.gcHook();
} }
} }
@@ -1034,7 +1119,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult); this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
} }
//<-- Sync
async initializeDatabase() { async initializeDatabase() {
await this.openDatabase(); await this.openDatabase();
@@ -1049,19 +1133,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(e.path) == -1); const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(e.path) == -1);
const onlyInDatabase = filesDatabase.filter((e) => filesStorageName.indexOf(e) == -1); const onlyInDatabase = filesDatabase.filter((e) => filesStorageName.indexOf(e) == -1);
//simply realize it
const onlyInStorageNames = onlyInStorage.map((e) => e.path); const onlyInStorageNames = onlyInStorage.map((e) => e.path);
//have to sync below..
const syncFiles = filesStorage.filter((e) => onlyInStorageNames.indexOf(e.path) == -1); const syncFiles = filesStorage.filter((e) => onlyInStorageNames.indexOf(e.path) == -1);
// just write to DB from storage.
for (let v of onlyInStorage) { for (let v of onlyInStorage) {
await this.updateIntoDB(v); await this.updateIntoDB(v);
} }
// simply realize it
for (let v of onlyInDatabase) { for (let v of onlyInDatabase) {
await this.pullFile(v, filesStorage); await this.pullFile(v, filesStorage);
} }
// have to sync below..
for (let v of syncFiles) { for (let v of syncFiles) {
await this.syncFileBetweenDBandStorage(v, filesStorage); await this.syncFileBetweenDBandStorage(v, filesStorage);
} }
@@ -1070,9 +1155,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.addLog(`delete folder:${folder.path}`); this.addLog(`delete folder:${folder.path}`);
for (var v of folder.children) { for (var v of folder.children) {
let entry = v as TFile & TFolder; let entry = v as TFile & TFolder;
this.addLog(`->entry:${entry.path}`); this.addLog(`->entry:${entry.path}`, LOG_LEVEL.VERBOSE);
if (entry.children) { if (entry.children) {
this.addLog(`->is dir`); this.addLog(`->is dir`, LOG_LEVEL.VERBOSE);
await this.deleteFolderOnDB(entry); await this.deleteFolderOnDB(entry);
try { try {
await this.app.vault.delete(entry); await this.app.vault.delete(entry);
@@ -1080,12 +1165,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (ex.code && ex.code == "ENOENT") { if (ex.code && ex.code == "ENOENT") {
//NO OP. //NO OP.
} else { } else {
this.addLog(`error while delete filder:${entry.path}`); this.addLog(`error while delete filder:${entry.path}`, LOG_LEVEL.NOTICE);
this.addLog(ex); this.addLog(ex);
} }
} }
} else { } else {
this.addLog(`->is file`); this.addLog(`->is file`, LOG_LEVEL.VERBOSE);
await this.deleteFromDB(entry); await this.deleteFromDB(entry);
} }
} }
@@ -1095,7 +1180,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (ex.code && ex.code == "ENOENT") { if (ex.code && ex.code == "ENOENT") {
//NO OP. //NO OP.
} else { } else {
this.addLog(`error while delete filder:${folder.path}`); this.addLog(`error while delete filder:${folder.path}`, LOG_LEVEL.NOTICE);
this.addLog(ex); this.addLog(ex);
} }
} }
@@ -1116,7 +1201,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
// --> conflict resolving // --> conflict resolving
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> { async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
try { try {
let doc = await this.localDatabase.getDatabaseDoc(path, { rev: rev }); let doc = await this.localDatabase.getDBEntry(path, { rev: rev });
if (doc === false) return false; if (doc === false) return false;
let data = doc.data; let data = doc.data;
if (doc.datatype == "newnote") { if (doc.datatype == "newnote") {
@@ -1137,42 +1222,44 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
return false; return false;
} }
/**
* Getting file conflicted status.
* @param path the file location
* @returns true -> resolved, false -> nothing to do, or check result.
*/
async getConflictedStatus(path: string): Promise<diff_check_result> { async getConflictedStatus(path: string): Promise<diff_check_result> {
let test: LoadedEntry & PouchDB.Core.GetMeta = null; let test = await this.localDatabase.getDBEntry(path, { conflicts: true });
try { if (test === false) return false;
let testDoc = await this.localDatabase.getDatabaseDoc(path, { conflicts: true });
if (testDoc === false) return false;
if ("_rev" in testDoc) {
test = testDoc as any;
}
} catch (ex) {
if (ex.status && ex.status == 404) {
this.addLog(`Getting conflicted status, but there was not ${path}`);
// NO OP.
} else {
throw ex;
}
}
if (test == null) return false; if (test == null) return false;
if (!test._conflicts) return false; if (!test._conflicts) return false;
if (test._conflicts.length == 0) return false; if (test._conflicts.length == 0) return false;
// should be two or more conflicts; // should be one or more conflicts;
let leftLeaf = await this.getConflictedDoc(path, test._rev); let leftLeaf = await this.getConflictedDoc(path, test._rev);
let rightLeaf = await this.getConflictedDoc(path, test._conflicts[0]); let rightLeaf = await this.getConflictedDoc(path, test._conflicts[0]);
if (leftLeaf === false) return false; if (leftLeaf == false) {
if (rightLeaf === false) return false; // what's going on..
this.addLog(`could not get current revisions:${path}`, LOG_LEVEL.NOTICE);
return false;
}
if (rightLeaf == false) {
// Conflicted item could not load, delete this.
await this.localDatabase.deleteDBEntry(path, { rev: test._conflicts[0] });
await this.pullFile(path, null, true);
this.addLog(`could not get old revisions, automaticaly used newer one:${path}`, LOG_LEVEL.NOTICE);
return true;
}
// first,check for same contents // first,check for same contents
if (leftLeaf.data == rightLeaf.data) { if (leftLeaf.data == rightLeaf.data) {
let leaf = leftLeaf; let leaf = leftLeaf;
if (leftLeaf.mtime > rightLeaf.mtime) { if (leftLeaf.mtime > rightLeaf.mtime) {
leaf = rightLeaf; leaf = rightLeaf;
} }
await this.localDatabase.deleteDBEntry(path, leaf.rev); await this.localDatabase.deleteDBEntry(path, { rev: leaf.rev });
await this.pullFile(path, null, true); await this.pullFile(path, null, true);
this.addLog(`automaticaly merged:${path}`); this.addLog(`automaticaly merged:${path}`);
return true; return true;
// }
} }
// make diff.
let dmp = new diff_match_patch(); let dmp = new diff_match_patch();
var diff = dmp.diff_main(leftLeaf.data, rightLeaf.data); var diff = dmp.diff_main(leftLeaf.data, rightLeaf.data);
dmp.diff_cleanupSemantic(diff); dmp.diff_cleanupSemantic(diff);
@@ -1190,41 +1277,58 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//auto resolved, but need check again; //auto resolved, but need check again;
setTimeout(() => { setTimeout(() => {
this.showIfConflicted(file); this.showIfConflicted(file);
}, 50); }, 500);
return; return;
} }
//there conflicts, and have to resolve ; //there conflicts, and have to resolve ;
let leaf = this.app.workspace.activeLeaf; let leaf = this.app.workspace.activeLeaf;
if (leaf) { if (leaf) {
new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => { new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => {
let testDoc = await this.localDatabase.getDBEntry(file.path, { conflicts: true });
if (testDoc === false) return;
if (!testDoc._conflicts) {
this.addLog("something went wrong on merging.", LOG_LEVEL.NOTICE);
return;
}
let toDelete = selected; let toDelete = selected;
if (toDelete == null) {
//concat both,
if (conflictCheckResult !== false && conflictCheckResult !== true) {
// write data,and delete both old rev.
let p = conflictCheckResult.diff.map((e) => e[1]).join("");
await this.app.vault.modify(file, p);
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev });
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev });
}
return;
}
if (toDelete == "") { if (toDelete == "") {
return; return;
} }
this.addLog(`resolved conflict:${file.path}`); this.addLog(`resolved conflict:${file.path}`);
await this.localDatabase.deleteDBEntry(file.path, toDelete); await this.localDatabase.deleteDBEntry(file.path, { rev: toDelete });
await this.pullFile(file.path, null, true); await this.pullFile(file.path, null, true);
setTimeout(() => { setTimeout(() => {
//resolved, check again. //resolved, check again.
this.showIfConflicted(file); this.showIfConflicted(file);
}, 50); }, 500);
}).open(); }).open();
} }
} }
async pullFile(filename: string, fileList?: TFile[], force?: boolean) { async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string) {
if (!fileList) { if (!fileList) {
fileList = this.app.vault.getFiles(); fileList = this.app.vault.getFiles();
} }
let targetFiles = fileList.filter((e) => e.path == filename); let targetFiles = fileList.filter((e) => e.path == filename);
if (targetFiles.length == 0) { if (targetFiles.length == 0) {
//have to create; //have to create;
let doc = await this.localDatabase.getDatabaseDoc(filename); let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null);
if (doc === false) return; if (doc === false) return;
await this.doc2storage_create(doc, force); await this.doc2storage_create(doc, force);
} else if (targetFiles.length == 1) { } else if (targetFiles.length == 1) {
//normal case //normal case
let file = targetFiles[0]; let file = targetFiles[0];
let doc = await this.localDatabase.getDatabaseDoc(filename); let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null);
if (doc === false) return; if (doc === false) return;
await this.doc2storate_modify(doc, file, force); await this.doc2storate_modify(doc, file, force);
} else { } else {
@@ -1234,7 +1338,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//when to opened file; //when to opened file;
} }
async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) { async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) {
let doc = await this.localDatabase.getDatabaseDoc(file.path); let doc = await this.localDatabase.getDBEntry(file.path);
if (doc === false) return; if (doc === false) return;
if (file.stat.mtime > doc.mtime) { if (file.stat.mtime > doc.mtime) {
//newer local file. //newer local file.
@@ -1268,22 +1372,22 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
mtime: file.stat.mtime, mtime: file.stat.mtime,
size: file.stat.size, size: file.stat.size,
children: [], children: [],
datatype: datatype datatype: datatype,
}; };
//From here //From here
let old = await this.localDatabase.getDatabaseDoc(fullpath); let old = await this.localDatabase.getDBEntry(fullpath);
if (old !== false) { if (old !== false) {
let oldData = { data: old.data, deleted: old._deleted }; let oldData = { data: old.data, deleted: old._deleted };
let newData = { data: d.data, deleted: d._deleted }; let newData = { data: d.data, deleted: d._deleted };
if (JSON.stringify(oldData) == JSON.stringify(newData)) { if (JSON.stringify(oldData) == JSON.stringify(newData)) {
this.addLog("no changed" + fullpath + (d._deleted ? " (deleted)" : "")); this.addLog("not changed:" + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
return; return;
} }
// d._rev = old._rev; // d._rev = old._rev;
} }
let ret = await this.localDatabase.putDBEntry(d); let ret = await this.localDatabase.putDBEntry(d);
this.addLog("put database:" + fullpath + "(" + datatype + ")"); this.addLog("put database:" + fullpath + "(" + datatype + ") ");
if (this.settings.syncOnSave) { if (this.settings.syncOnSave) {
await this.replicate(); await this.replicate();
} }
@@ -1398,6 +1502,12 @@ class ConflictResolveModal extends Modal {
this.close(); this.close();
}); });
}); });
contentEl.createEl("button", { text: "Concat both" }, (e) => {
e.addEventListener("click", async () => {
await this.callback(null);
this.close();
});
});
contentEl.createEl("button", { text: "Not now" }, (e) => { contentEl.createEl("button", { text: "Not now" }, (e) => {
e.addEventListener("click", async () => { e.addEventListener("click", async () => {
this.close(); this.close();
@@ -1424,10 +1534,10 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
password: this.plugin.settings.couchDB_PASSWORD, password: this.plugin.settings.couchDB_PASSWORD,
}); });
if (db === false) { if (db === false) {
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI}`, true); this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI}`, LOG_LEVEL.NOTICE);
return; return;
} }
this.plugin.addLog(`Connected to ${db.info.db_name}`, true); this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL.NOTICE);
} }
display(): void { display(): void {
let { containerEl } = this; let { containerEl } = this;
@@ -1503,8 +1613,8 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.gcDelay + "") .setValue(this.plugin.settings.gcDelay + "")
.onChange(async (value) => { .onChange(async (value) => {
let v = Number(value); let v = Number(value);
if (isNaN(v) || v < 200 || v > 5000) { if (isNaN(v) || v > 5000) {
return 30; return 0;
//text.inputEl.va; //text.inputEl.va;
} }
this.plugin.settings.gcDelay = v; this.plugin.settings.gcDelay = v;
@@ -1521,6 +1631,15 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerEl)
.setName("Verbose Log")
.setDesc("Show verbose log ")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showVerboseLog).onChange(async (value) => {
this.plugin.settings.showVerboseLog = value;
await this.plugin.saveSettings();
})
);
if (this.plugin.settings.versionUpFlash != "") { if (this.plugin.settings.versionUpFlash != "") {
let c = containerEl.createEl("div", { text: this.plugin.settings.versionUpFlash }); let c = containerEl.createEl("div", { text: this.plugin.settings.versionUpFlash });
c.createEl("button", { text: "I got it and updated." }, (e) => { c.createEl("button", { text: "I got it and updated." }, (e) => {
@@ -1530,7 +1649,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
c.remove(); c.remove();
}); });
}); });
c.addClass("op-warn") c.addClass("op-warn");
} }
// containerEl.createDiv(this.plugin.settings.versionUpFlash); // containerEl.createDiv(this.plugin.settings.versionUpFlash);
new Setting(containerEl) new Setting(containerEl)
@@ -1593,28 +1712,15 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}); });
text.inputEl.setAttribute("type", "number"); text.inputEl.setAttribute("type", "number");
}); });
new Setting(containerEl) new Setting(containerEl).setName("Local Database Operations").addButton((button) =>
.setName("Local Database Operations")
.addButton((button) =>
button button
.setButtonText("Reset local database") .setButtonText("Reset local database")
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
await this.plugin.resetLocalDatabase(); await this.plugin.resetLocalDatabase();
//await this.test();
})
)
.addButton((button) =>
button
.setButtonText("Reset local files")
.setDisabled(false)
.onClick(async () => {
//await this.test();
}) })
); );
new Setting(containerEl) new Setting(containerEl).setName("Re-init").addButton((button) =>
.setName("Re-init")
.addButton((button) =>
button button
.setButtonText("Init Database again") .setButtonText("Init Database again")
.setDisabled(false) .setDisabled(false)
@@ -1624,24 +1730,20 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
); );
new Setting(containerEl) new Setting(containerEl).setName("Garbage Collect").addButton((button) =>
.setName("Garbage Collect")
.addButton((button) =>
button button
.setButtonText("Garbage Collection") .setButtonText("Garbage Collection")
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
await this.plugin.garbageCollect(); await this.plugin.garbageCollect();
//await this.test();
}) })
) );
new Setting(containerEl).setName("Remote Database Operations").addButton((button) => new Setting(containerEl).setName("Remote Database Operations").addButton((button) =>
button button
.setButtonText("Reset remote database") .setButtonText("Reset remote database")
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
await this.plugin.tryResetRemoteDatabase(); await this.plugin.tryResetRemoteDatabase();
//await this.test();
}) })
); );
new Setting(containerEl).setName("Remote Database Operations").addButton((button) => new Setting(containerEl).setName("Remote Database Operations").addButton((button) =>
@@ -1650,7 +1752,6 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
await this.plugin.tryResetRemoteDatabase(); await this.plugin.tryResetRemoteDatabase();
//await this.test();
}) })
); );
} }

View File

@@ -1,7 +1,7 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Obsidian Live sync", "name": "Obsidian Live sync",
"version": "0.1.0", "version": "0.1.1",
"minAppVersion": "0.9.12", "minAppVersion": "0.9.12",
"description": "obsidian Live synchronization plugin.", "description": "obsidian Live synchronization plugin.",
"author": "vorotamoroz", "author": "vorotamoroz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.10.0", "version": "0.1.1",
"description": "obsidian Live synchronization plugin.", "description": "obsidian Live synchronization plugin.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {