1
0
mirror of https://github.com/vrtmrz/obsidian-livesync.git synced 2025-02-07 19:30:08 +02:00

Bug fixed and new feature implemented

- Synchronization Timing problem fixed
- Performance improvement of handling large files
- Timeout for collecting leaves extended
- Periodic synchronization implemented
- Dumping document information implemented.
- Folder watching problem fixed.
- Delay vault watching for database ready.
This commit is contained in:
vorotamoroz 2021-11-10 18:07:09 +09:00
parent 155439ed56
commit 9facb57760
4 changed files with 281 additions and 87 deletions

360
main.ts
View File

@ -1,4 +1,4 @@
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath, TAbstractFile } from "obsidian";
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView } from "obsidian";
import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import xxhash from "xxhash-wasm";
@ -11,7 +11,7 @@ const MAX_DOC_SIZE_BIN = 102400; // 100kb
const VER = 10;
const RECENT_MOFIDIED_DOCS_QTY = 30;
const LEAF_WAIT_TIMEOUT = 30000; // in synchronization, waiting missing leaf time out.
const LEAF_WAIT_TIMEOUT = 90000; // in synchronization, waiting missing leaf time out.
const LOG_LEVEL = {
VERBOSE: 1,
INFO: 10,
@ -31,6 +31,7 @@ interface ObsidianLiveSyncSettings {
liveSync: boolean;
syncOnSave: boolean;
syncOnStart: boolean;
syncOnFileOpen: boolean;
savingDelay: number;
lessInformationInLog: boolean;
gcDelay: number;
@ -40,6 +41,8 @@ interface ObsidianLiveSyncSettings {
showVerboseLog: boolean;
suspendFileWatching: boolean;
trashInsteadDelete: boolean;
periodicReplication: boolean;
periodicReplicationInterval: number;
}
const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
@ -58,6 +61,9 @@ const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
showVerboseLog: false,
suspendFileWatching: false,
trashInsteadDelete: false,
periodicReplication: false,
periodicReplicationInterval: 60,
syncOnFileOpen: false,
};
interface Entry {
_id: string;
@ -159,7 +165,7 @@ type Credential = {
type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;
//-->Functions.
function arrayBufferToBase64(buffer: ArrayBuffer) {
function arrayBufferToBase64Old(buffer: ArrayBuffer) {
var binary = "";
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
@ -168,6 +174,18 @@ function arrayBufferToBase64(buffer: ArrayBuffer) {
}
return window.btoa(binary);
}
// Ten times faster.
function arrayBufferToBase64(buffer: ArrayBuffer): Promise<string> {
return new Promise((res) => {
var blob = new Blob([buffer], { type: "application/octet-binary" });
var reader = new FileReader();
reader.onload = function (evt) {
var dataurl = evt.target.result.toString();
res(dataurl.substr(dataurl.indexOf(",") + 1));
};
reader.readAsDataURL(blob);
});
}
function base64ToArrayBuffer(base64: string): ArrayBuffer {
try {
@ -315,6 +333,7 @@ class LocalPouchDB {
settings: ObsidianLiveSyncSettings;
localDatabase: PouchDB.Database<EntryDoc>;
nodeid: string = "";
isReady: boolean = false;
recentModifiedDocs: string[] = [];
h32: (input: string, seed?: number) => string;
@ -338,10 +357,17 @@ class LocalPouchDB {
this.dbname = dbname;
this.settings = settings;
this.initializeDatabase();
// this.initializeDatabase();
}
close() {
this.localDatabase.close();
this.isReady = false;
if (this.changeHandler != null) {
this.changeHandler.cancel();
this.changeHandler.removeAllListeners();
}
if (this.localDatabase != null) {
this.localDatabase.close();
}
}
status() {
if (this.syncHandler == null) {
@ -370,6 +396,7 @@ class LocalPouchDB {
changeHandler: PouchDB.Core.Changes<{}> = null;
async initializeDatabase() {
await this.prepareHashFunctions();
if (this.localDatabase != null) this.localDatabase.close();
if (this.changeHandler != null) {
this.changeHandler.cancel();
@ -380,6 +407,9 @@ class LocalPouchDB {
revs_limit: 100,
deterministic_revs: true,
});
Logger("Database Info");
Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE);
// initialize local node information.
let nodeinfo: EntryNodeInfo = await resolveWithIgnoreKnownError<EntryNodeInfo>(this.localDatabase.get(NODEINFO_DOCID), {
_id: NODEINFO_DOCID,
@ -390,6 +420,9 @@ class LocalPouchDB {
nodeinfo.nodeid = Math.random().toString(36).slice(-10);
await this.localDatabase.put(nodeinfo);
}
this.localDatabase.on("close", () => {
this.isReady = false;
});
this.nodeid = nodeinfo.nodeid;
// Traceing the leaf id
@ -405,7 +438,7 @@ class LocalPouchDB {
this.docSeq = `${e.seq}`;
});
this.changeHandler = changes;
await this.prepareHashFunctions();
this.isReady = true;
}
async prepareHashFunctions() {
@ -514,6 +547,7 @@ class LocalPouchDB {
children: [],
datatype: "newnote",
};
return doc;
}
} catch (ex) {
if (ex.status && ex.status == 404) {
@ -523,7 +557,7 @@ class LocalPouchDB {
}
return false;
}
async getDBEntry(id: string, opt?: PouchDB.Core.GetOptions, retryCount = 5): Promise<false | LoadedEntry> {
async getDBEntry(id: string, opt?: PouchDB.Core.GetOptions, dump = false): Promise<false | LoadedEntry> {
try {
let obj: EntryDocResponse = null;
if (opt) {
@ -555,15 +589,28 @@ class LocalPouchDB {
if (typeof this.corruptedEntries[doc._id] != "undefined") {
delete this.corruptedEntries[doc._id];
}
if (dump) {
Logger(`Simple doc`);
Logger(doc);
}
return doc;
// simple note
}
if (obj.type == "newnote" || obj.type == "plain") {
// search childrens
try {
if (dump) {
Logger(`Enhanced doc`);
Logger(obj);
}
let childrens;
try {
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e)));
if (dump) {
Logger(`childrens:`);
Logger(childrens);
}
} catch (ex) {
Logger(`Something went wrong on reading elements of ${obj._id} from database.`, LOG_LEVEL.NOTICE);
this.corruptedEntries[obj._id] = obj;
@ -583,6 +630,10 @@ class LocalPouchDB {
datatype: obj.type,
_conflicts: obj._conflicts,
};
if (dump) {
Logger(`therefore:`);
Logger(doc);
}
if (typeof this.corruptedEntries[doc._id] != "undefined") {
delete this.corruptedEntries[doc._id];
}
@ -693,6 +744,18 @@ class LocalPouchDB {
Logger(`deleteDBEntryPrefix:deleted ${deleteCount} items, skipped ${notfound}`);
return true;
}
isPlainText(filename: string): boolean {
if (filename.endsWith(".md")) return true;
if (filename.endsWith(".txt")) return true;
if (filename.endsWith(".svg")) return true;
if (filename.endsWith(".html")) return true;
if (filename.endsWith(".csv")) return true;
if (filename.endsWith(".css")) return true;
if (filename.endsWith(".js")) return true;
if (filename.endsWith(".xml")) return true;
return false;
}
async putDBEntry(note: LoadedEntry) {
let leftData = note.data;
let savenNotes = [];
@ -702,10 +765,11 @@ class LocalPouchDB {
let pieceSize = MAX_DOC_SIZE_BIN;
let plainSplit = false;
let cacheUsed = 0;
if (note._id.endsWith(".md")) {
if (this.isPlainText(note._id)) {
pieceSize = MAX_DOC_SIZE;
plainSplit = true;
}
let newLeafs: EntryLeaf[] = [];
do {
// To keep low bandwith and database size,
// Dedup pieces on database.
@ -715,11 +779,11 @@ class LocalPouchDB {
// 3. \r\n\r\n should break
// 4. \n# should break.
let cPieceSize = pieceSize;
let minimumChunkSize = this.settings.minimumChunkSize;
if (minimumChunkSize < 10) minimumChunkSize = 10;
let longLineThreshold = this.settings.longLineThreshold;
if (longLineThreshold < 100) longLineThreshold = 100;
if (plainSplit) {
let minimumChunkSize = this.settings.minimumChunkSize;
if (minimumChunkSize < 10) minimumChunkSize = 10;
let longLineThreshold = this.settings.longLineThreshold;
if (longLineThreshold < 100) longLineThreshold = 100;
cPieceSize = 0;
// lookup for next splittion .
// we're standing on "\n"
@ -748,11 +812,13 @@ class LocalPouchDB {
} while (cPieceSize < minimumChunkSize);
}
// piece size determined.
let piece = leftData.substring(0, cPieceSize);
leftData = leftData.substring(cPieceSize);
processed++;
let leafid = "";
// Get has of piece.
// Get hash of piece.
let hashedPiece: string = "";
let hashQ: number = 0; // if hash collided, **IF**, count it up.
let tryNextHash = false;
@ -803,53 +869,72 @@ class LocalPouchDB {
data: piece,
type: "leaf",
};
let result = await this.localDatabase.put(d);
this.updateRecentModifiedDocs(result.id, result.rev, d._deleted);
if (result.ok) {
Logger(`save ok:id:${result.id} rev:${result.rev}`, LOG_LEVEL.VERBOSE);
this.hashCache[piece] = leafid;
this.hashCacheRev[leafid] = piece;
made++;
} else {
Logger("save faild");
}
newLeafs.push(d);
this.hashCache[piece] = leafid;
this.hashCacheRev[leafid] = piece;
made++;
} else {
skiped++;
}
}
savenNotes.push(leafid);
} while (leftData != "");
Logger(`note content saven, pieces:${processed} new:${made}, skip:${skiped}, cache:${cacheUsed}`);
let newDoc: PlainEntry | NewEntry = {
NewNote: true,
children: savenNotes,
_id: note._id,
ctime: note.ctime,
mtime: note.mtime,
size: note.size,
type: plainSplit ? "plain" : "newnote",
};
// Here for upsert logic,
try {
let old = await this.localDatabase.get(newDoc._id);
if (!old.type || old.type == "notes" || old.type == "newnote" || old.type == "plain") {
// simple use rev for new doc
newDoc._rev = old._rev;
}
} catch (ex) {
if (ex.status && ex.status == 404) {
// NO OP/
} else {
throw ex;
let saved = true;
if (newLeafs.length > 0) {
try {
let result = await this.localDatabase.bulkDocs(newLeafs);
for (let 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 {
Logger(`save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE);
Logger(item);
this.disposeHashCache();
saved = false;
}
}
} catch (ex) {
Logger("ERROR ON SAVING LEAVES ");
Logger(ex);
saved = false;
}
}
let r = await this.localDatabase.put(newDoc);
this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted);
if (typeof this.corruptedEntries[note._id] != "undefined") {
delete this.corruptedEntries[note._id];
if (saved) {
Logger(`note content saven, pieces:${processed} new:${made}, skip:${skiped}, cache:${cacheUsed}`);
let newDoc: PlainEntry | NewEntry = {
NewNote: true,
children: savenNotes,
_id: note._id,
ctime: note.ctime,
mtime: note.mtime,
size: note.size,
type: plainSplit ? "plain" : "newnote",
};
// Here for upsert logic,
try {
let old = await this.localDatabase.get(newDoc._id);
if (!old.type || old.type == "notes" || old.type == "newnote" || old.type == "plain") {
// simple use rev for new doc
newDoc._rev = old._rev;
}
} catch (ex) {
if (ex.status && ex.status == 404) {
// NO OP/
} else {
throw ex;
}
}
let r = await this.localDatabase.put(newDoc);
this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted);
if (typeof this.corruptedEntries[note._id] != "undefined") {
delete this.corruptedEntries[note._id];
}
Logger(`note saven:${newDoc._id}:${r.rev}`);
} else {
Logger(`note coud not saved:${note._id}`);
}
Logger(`note saven:${newDoc._id}:${r.rev}`);
}
syncHandler: PouchDB.Replication.Sync<{}> = null;
@ -900,12 +985,12 @@ class LocalPouchDB {
this.docSent += e.docs_written;
this.docArrived += e.docs_read;
this.updateInfo();
Logger(`sending..:${e.docs.length}`);
Logger(`replicateAllToServer: sending..:${e.docs.length}`);
})
.on("complete", async (info) => {
this.syncStatus = "COMPLETED";
this.updateInfo();
Logger("Completed", LOG_LEVEL.NOTICE);
Logger("replicateAllToServer: Completed", LOG_LEVEL.NOTICE);
replicate.cancel();
replicate.removeAllListeners();
res(true);
@ -913,13 +998,21 @@ class LocalPouchDB {
.on("error", (e) => {
this.syncStatus = "ERRORED";
this.updateInfo();
Logger("Pulling Replication error", LOG_LEVEL.NOTICE);
Logger("replicateAllToServer: Pulling Replication error", LOG_LEVEL.INFO);
Logger(e);
replicate.cancel();
replicate.removeAllListeners();
rej(e);
});
});
}
async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<{}>[]) => Promise<void>) {
if (!this.isReady) {
Logger("Database is not ready.");
return false;
}
if (setting.versionUpFlash != "") {
new Notice("Open settings and check message, please.");
return;
@ -973,11 +1066,15 @@ class LocalPouchDB {
let db = dbret.db;
//replicate once
this.syncStatus = "CONNECTED";
Logger("Pull before replicate.");
Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE);
Logger(await db.info(), LOG_LEVEL.VERBOSE);
let replicate = this.localDatabase.replicate.from(db, syncOptionBase);
replicate
.on("active", () => {
this.syncStatus = "CONNECTED";
this.updateInfo();
Logger("Replication pull activated.");
})
.on("change", async (e) => {
// when in first run, replication will send us tombstone data
@ -1004,6 +1101,7 @@ class LocalPouchDB {
this.syncHandler.cancel();
this.syncHandler.removeAllListeners();
}
Logger("Replication pull completed.");
this.syncHandler = this.localDatabase.sync(db, syncOption);
this.syncHandler
.on("active", () => {
@ -1051,7 +1149,13 @@ class LocalPouchDB {
.on("error", (e) => {
this.syncStatus = "ERRORED";
this.updateInfo();
Logger("Pulling Replication error", LOG_LEVEL.NOTICE);
Logger("Pulling Replication error", LOG_LEVEL.INFO);
replicate.cancel();
replicate.removeAllListeners();
this.syncHandler.cancel();
this.syncHandler.removeAllListeners();
this.syncHandler = null;
// debugger;
Logger(e);
});
}
@ -1070,9 +1174,11 @@ class LocalPouchDB {
async resetDatabase() {
if (this.changeHandler != null) {
this.changeHandler.removeAllListeners();
this.changeHandler.cancel();
}
await this.closeReplication();
this.isReady = false;
await this.localDatabase.destroy();
this.localDatabase = null;
await this.initializeDatabase();
@ -1080,7 +1186,6 @@ class LocalPouchDB {
Logger("Local Database Reset", LOG_LEVEL.NOTICE);
}
async tryResetRemoteDatabase(setting: ObsidianLiveSyncSettings) {
await this.closeReplication();
await this.closeReplication();
let uri = setting.couchDB_URI;
let auth: Credential = {
@ -1181,9 +1286,11 @@ class LocalPouchDB {
let readCount = 0;
let hashPieces: string[] = [];
let usedPieces: string[] = [];
Logger("Collecting Garbage");
do {
let result = await this.localDatabase.allDocs({ include_docs: true, skip: c, limit: 100, conflicts: true });
let result = await this.localDatabase.allDocs({ include_docs: true, skip: c, limit: 500, conflicts: true });
readCount = result.rows.length;
Logger("checked:" + readCount);
if (readCount > 0) {
//there are some result
for (let v of result.rows) {
@ -1209,13 +1316,21 @@ class LocalPouchDB {
c += readCount;
} while (readCount != 0);
// items collected.
Logger("Finding unused pieces");
const garbages = hashPieces.filter((e) => usedPieces.indexOf(e) == -1);
let deleteCount = 0;
Logger("we have to delete:" + garbages.length);
let deleteDoc: EntryDoc[] = [];
for (let v of garbages) {
try {
let item = await this.localDatabase.get(v);
item._deleted = true;
await this.localDatabase.put(item);
deleteDoc.push(item);
if (deleteDoc.length > 50) {
await this.localDatabase.bulkDocs(deleteDoc);
deleteDoc = [];
Logger("delete:" + deleteCount);
}
deleteCount++;
} catch (ex) {
if (ex.status && ex.status == 404) {
@ -1225,6 +1340,9 @@ class LocalPouchDB {
}
}
}
if (deleteDoc.length > 0) {
await this.localDatabase.bulkDocs(deleteDoc);
}
Logger(`GC:deleted ${deleteCount} items.`);
}
}
@ -1246,6 +1364,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.settings.liveSync = false;
this.settings.syncOnSave = false;
this.settings.syncOnStart = false;
this.settings.periodicReplication = false;
this.settings.versionUpFlash = "I changed specifications incompatiblly, so when you enable sync again, be sure to made version up all nother devides.";
this.saveSettings();
}
@ -1295,30 +1414,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), delay, false);
this.watchWindowVisiblity = debounce(this.watchWindowVisiblity.bind(this), delay, false);
this.registerWatchEvents();
this.parseReplicationResult = this.parseReplicationResult.bind(this);
this.periodicSync = this.periodicSync.bind(this);
this.setPeriodicSync = this.setPeriodicSync.bind(this);
// this.registerWatchEvents();
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
this.app.workspace.onLayoutReady(async () => {
await this.initializeDatabase();
this.realizeSettingSyncMode();
if (this.settings.syncOnStart) {
await this.replicate(false);
}
this.registerWatchEvents();
});
// when in mobile, too long suspended , connection won't back if setting retry:true
this.registerInterval(
window.setInterval(async () => {
if (this.settings.liveSync) {
await this.localDatabase.closeReplication();
if (this.settings.liveSync) {
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
}
}
}, 60 * 1000)
);
this.addCommand({
id: "livesync-replicate",
name: "Replicate now",
@ -1326,6 +1434,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.replicate();
},
});
this.addCommand({
id: "livesync-dump",
name: "Dump informations of this doc ",
editorCallback: (editor: Editor, view: MarkdownView) => {
//this.replicate();
this.localDatabase.getDBEntry(view.file.path, {}, true);
},
});
// this.addCommand({
// id: "livesync-test",
// name: "test reset db and replicate",
@ -1356,14 +1472,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.saveSettings();
},
});
this.watchWindowVisiblity = this.watchWindowVisiblity.bind(this);
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
}
onunload() {
if (this.gcTimerHandler != null) {
clearTimeout(this.gcTimerHandler);
this.gcTimerHandler = null;
}
this.clearPeriodicSync();
this.localDatabase.closeReplication();
this.localDatabase.close();
window.removeEventListener("visibilitychange", this.watchWindowVisiblity);
@ -1375,6 +1490,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.localDatabase.close();
}
let vaultName = this.app.vault.getName();
Logger("Open Database...");
this.localDatabase = new LocalPouchDB(this.settings, vaultName);
this.localDatabase.updateInfo = () => {
this.refreshStatusText();
@ -1392,6 +1508,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async saveSettings() {
await this.saveData(this.settings);
this.localDatabase.settings = this.settings;
this.realizeSettingSyncMode();
}
gcTimerHandler: any = null;
gcHook() {
@ -1412,6 +1529,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
this.registerEvent(this.app.vault.on("create", this.watchVaultChange));
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
}
watchWindowVisiblity() {
@ -1422,6 +1540,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
let isHidden = document.hidden;
if (isHidden) {
this.localDatabase.closeReplication();
this.clearPeriodicSync();
} else {
if (this.settings.liveSync) {
await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
@ -1429,6 +1548,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.settings.syncOnStart) {
await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
}
if (this.settings.periodicReplication) {
this.setPeriodicSync();
}
}
this.gcHook();
}
@ -1439,6 +1561,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
async watchWorkspaceOpenAsync(file: TFile) {
if (file == null) return;
if (this.settings.syncOnFileOpen) {
await this.replicate();
}
this.localDatabase.disposeHashCache();
await this.showIfConflicted(file);
this.gcHook();
@ -1453,10 +1578,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
batchFileChange: string[] = [];
async watchVaultChangeAsync(file: TFile, ...args: any[]) {
await this.updateIntoDB(file);
this.gcHook();
if (file instanceof TFile) {
await this.updateIntoDB(file);
this.gcHook();
}
}
watchVaultDelete(file: TFile | TFolder) {
console.log(`${file.path} delete`);
if (this.settings.suspendFileWatching) return;
this.watchVaultDeleteAsync(file);
}
@ -1518,7 +1646,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
this.gcHook();
}
addLogHook: () => void = null;
//--> Basic document Functions
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) {
@ -1540,6 +1668,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (level >= LOG_LEVEL.NOTICE) {
new Notice(messagecontent);
}
if (this.addLogHook != null) this.addLogHook();
}
async ensureDirectory(fullpath: string) {
@ -1694,6 +1823,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
}
periodicSyncHandler: NodeJS.Timer = null;
//---> Sync
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
this.refreshStatusText();
@ -1714,12 +1844,29 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.gcHook();
}
}
clearPeriodicSync() {
if (this.periodicSyncHandler != null) {
clearInterval(this.periodicSyncHandler);
this.periodicSyncHandler = null;
}
}
setPeriodicSync() {
if (this.settings.periodicReplication && this.settings.periodicReplicationInterval > 0) {
this.clearPeriodicSync();
this.periodicSyncHandler = setInterval(() => this.periodicSync, Math.max(this.settings.periodicReplicationInterval, 30) * 1000);
}
}
async periodicSync() {
await this.replicate();
}
realizeSettingSyncMode() {
this.localDatabase.closeReplication();
if (this.settings.liveSync) {
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
this.refreshStatusText();
}
this.clearPeriodicSync();
this.setPeriodicSync();
}
refreshStatusText() {
let sent = this.localDatabase.docSent;
@ -1777,7 +1924,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const filesStorage = this.app.vault.getFiles();
const filesStorageName = filesStorage.map((e) => e.path);
const wf = await this.localDatabase.localDatabase.allDocs();
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:")).map((e) => normalizePath(e.id));
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && e.id != "obsydian_livesync_version").map((e) => normalizePath(e.id));
const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(e.path) == -1);
const onlyInDatabase = filesDatabase.filter((e) => filesStorageName.indexOf(e) == -1);
@ -1790,12 +1937,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.statusBar.setText(`UPDATE DATABASE`);
// just write to DB from storage.
for (let v of onlyInStorage) {
Logger(`Update into ${v.path}`);
await this.updateIntoDB(v);
}
// simply realize it
this.statusBar.setText(`UPDATE STORAGE`);
Logger("Writing files that only in database");
for (let v of onlyInDatabase) {
Logger(`Pull from db:${v}`);
await this.pullFile(v, filesStorage);
}
// have to sync below..
@ -1803,6 +1952,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
for (let v of syncFiles) {
await this.syncFileBetweenDBandStorage(v, filesStorage);
}
this.statusBar.setText(`NOW TRACKING!`);
Logger("Initialized");
}
async deleteFolderOnDB(folder: TFolder) {
@ -2028,7 +2178,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
let datatype: "plain" | "newnote" = "newnote";
if (file.extension != "md") {
let contentBin = await this.app.vault.readBinary(file);
content = arrayBufferToBase64(contentBin);
content = await arrayBufferToBase64(contentBin);
datatype = "newnote";
} else {
content = await this.app.vault.read(file);
@ -2111,13 +2261,13 @@ class LogDisplayModal extends Modal {
div.addClass("op-pre");
this.logEl = div;
this.updateLog = this.updateLog.bind(this);
// this.plugin.onLogChanged = this.updateLog;
this.plugin.addLogHook = this.updateLog;
this.updateLog();
}
onClose() {
let { contentEl } = this;
contentEl.empty();
// this.plugin.onLogChanged = null;
this.plugin.addLogHook = null;
}
}
class ConflictResolveModal extends Modal {
@ -2342,6 +2492,33 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.realizeSettingSyncMode();
})
);
new Setting(containerEl)
.setName("Periodic Sync")
.setDesc("Sync periodically")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.periodicReplication).onChange(async (value) => {
this.plugin.settings.periodicReplication = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Periodic sync intreval")
.setDesc("Interval (sec)")
.addText((text) => {
text.setPlaceholder("")
.setValue(this.plugin.settings.periodicReplicationInterval + "")
.onChange(async (value) => {
let v = Number(value);
if (isNaN(v) || v > 5000) {
return 0;
}
this.plugin.settings.periodicReplicationInterval = v;
await this.plugin.saveSettings();
});
text.inputEl.setAttribute("type", "number");
});
new Setting(containerEl)
.setName("Sync on Save")
.setDesc("When you save file, sync automatically")
@ -2351,6 +2528,15 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Sync on File Open")
.setDesc("When you open file, sync automatically")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.syncOnFileOpen).onChange(async (value) => {
this.plugin.settings.syncOnFileOpen = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Sync on Start")
.setDesc("Start synchronization on Obsidian started.")
@ -2451,6 +2637,12 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setWarning()
.setDisabled(false)
.onClick(async () => {
this.plugin.settings.liveSync = false;
this.plugin.settings.periodicReplication = false;
this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnStart = false;
await this.plugin.saveSettings();
await this.plugin.saveSettings();
await this.plugin.resetLocalDatabase();
await this.plugin.initializeDatabase();
await this.plugin.tryResetRemoteDatabase();
@ -2529,6 +2721,8 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
});
});
}
} else {
let cx = containerEl.createEl("div", { text: "There's no collupted data." });
}
}
}

View File

@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.1.13",
"version": "0.1.14",
"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",

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.1.13",
"version": "0.1.14",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.1.13",
"version": "0.1.14",
"license": "MIT",
"dependencies": {
"diff-match-patch": "^1.0.5",

View File

@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.1.13",
"version": "0.1.14",
"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",
"scripts": {