Compare commits

...

8 Commits

Author SHA1 Message Date
vorotamoroz
2b11be05ec Add new feature:
- Reread all files
2021-12-06 12:19:05 +09:00
vorotamoroz
0ee73860d1 Fixed:
- Make less file corruption.
- Some notice was not hidden automatically
2021-12-06 11:43:42 +09:00
vorotamoroz
ecec546f13 Improvements:
- Show sync status information inside the editor.

Fixed:
- Reduce the same messages on popup notifications
- show warning message when synchronization
2021-12-03 12:54:18 +09:00
vorotamoroz
4a8c76efb5 Tidy up:
- Plugin and setting table.
- Add new option showOwnPlugin.
- Change icons.
- Buttons in setting dialog.

Fixed: Could not sync the file that contains "$" in filename.
2021-11-29 16:31:29 +09:00
vorotamoroz
75ee63e573 Bumped 2021-11-26 17:27:35 +09:00
vorotamoroz
3435efaf89 Fixed:
- Removed waiting delay on fetching files from the database when launch.
- Database had not been closed right timings.
- Fixed wrong message.

Improved:
- Plugins and Settings sync is not compatible with Cloudant.
- "Restore from file" is added on "Corrupted data".
2021-11-26 07:52:15 +09:00
vorotamoroz
57f91eb407 Just added "yet".
(I will improve this feature)
2021-11-26 00:56:52 +09:00
vorotamoroz
50916aef0b add warning message. 2021-11-26 00:51:50 +09:00
7 changed files with 390 additions and 122 deletions

View File

@@ -143,6 +143,11 @@ You can dump saved note structure to `Dump informations of this doc`. Replace ev
Default values are 20 letters and 250 letters.
## Miscellaneous
### Show status inside editor
Show information inside the editor pane.
It would be useful for mobile.
## Hatch
From here, everything is under the hood. Please handle it with care.
@@ -160,6 +165,9 @@ The remote database indicates that has been unlocked Pattern 1.
When you mark all devices as resolved, you can unlock the database.
But, there's no problem even if you leave it as it is.
### Reread all files
Reread all files in the vault, and update them into the database if there's diff or could not read from the database.
### Drop history
Drop all histories on the local database and the remote database, and initialize When synchronization time has been prolonged to the new device or new vault, or database size became to be much larger. Try this.

View File

@@ -142,6 +142,12 @@ Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk s
改行文字と#を除き、すべて●に置換しても、アルゴリズムは有効に働きます。
デフォルトは20文字と、250文字です。
## Miscellaneous
その他の設定です
### Show status inside editor
同期の情報をエディター内に表示します。
モバイルで便利です。
## Hatch
ここから先は、困ったときに開ける蓋の中身です。注意して使用してください。
@@ -160,6 +166,9 @@ Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk s
ご使用のすべてのデバイスでロックを解除した場合は、データベースのロックを解除することができます。
ただし、このまま放置しても問題はありません。
### Reread all files
Vault内のファイルを全て読み込み直し、もし差分があったり、データベースから正常に読み込めなかったものに関して、データベースに反映します。
### Drop history
データベースに記録されている履歴を削除し、データベースを初期化します。
新しい端末や新しいVaultへの同期にやたらと時間がかかったり、データベースサイズが肥大化したりしてきた際に使用してください。

435
main.ts
View File

@@ -53,6 +53,8 @@ interface ObsidianLiveSyncSettings {
batchSave: boolean;
deviceAndVaultName: string;
usePluginSettings: boolean;
showOwnPlugins: boolean;
showStatusOnEditor: boolean;
}
const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
@@ -84,6 +86,8 @@ const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
batchSave: false,
deviceAndVaultName: "",
usePluginSettings: false,
showOwnPlugins: false,
showStatusOnEditor: false,
};
interface Entry {
@@ -351,7 +355,7 @@ const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): P
};
function isValidPath(filename: string): boolean {
let regex = /[\u0000-\u001f]|[\\"':?<>|*$]/g;
let regex = /[\u0000-\u001f]|[\\"':?<>|*]/g;
let x = filename.replace(regex, "_");
let win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi;
let sx = (x = x.replace(win, "/_"));
@@ -549,6 +553,13 @@ async function testCrypt() {
}
}
// <-- Encryption
const delay = (ms: number): Promise<void> => {
return new Promise((res) => {
setTimeout(() => {
res();
}, ms);
});
};
//<--Functions
class LocalPouchDB {
auth: Credential;
@@ -584,6 +595,7 @@ class LocalPouchDB {
// this.initializeDatabase();
}
close() {
Logger("Database closed (by close)");
this.isReady = false;
if (this.changeHandler != null) {
this.changeHandler.cancel();
@@ -645,6 +657,7 @@ class LocalPouchDB {
await this.localDatabase.put(nodeinfo);
}
this.localDatabase.on("close", () => {
Logger("Database closed.");
this.isReady = false;
});
this.nodeid = nodeinfo.nodeid;
@@ -663,6 +676,7 @@ class LocalPouchDB {
});
this.changeHandler = changes;
this.isReady = true;
Logger("Database is now ready.");
}
async prepareHashFunctions() {
@@ -688,7 +702,7 @@ class LocalPouchDB {
waitForLeafReady(id: string): Promise<boolean> {
return new Promise((res, rej) => {
// Set timeout.
let timer = setTimeout(() => rej(false), LEAF_WAIT_TIMEOUT);
let timer = setTimeout(() => rej(new Error(`Leaf timed out:${id}`)), LEAF_WAIT_TIMEOUT);
if (typeof this.leafArrivedCallbacks[id] == "undefined") {
this.leafArrivedCallbacks[id] = [];
}
@@ -699,7 +713,8 @@ class LocalPouchDB {
});
}
async getDBLeaf(id: string): Promise<string> {
async getDBLeaf(id: string, waitForReady: boolean): Promise<string> {
await this.waitForGCComplete();
// when in cache, use that.
if (this.hashCacheRev[id]) {
return this.hashCacheRev[id];
@@ -721,7 +736,7 @@ class LocalPouchDB {
}
throw new Error(`retrive leaf, but it was not leaf.`);
} catch (ex) {
if (ex.status && ex.status == 404) {
if (ex.status && ex.status == 404 && waitForReady) {
// just leaf is not ready.
// wait for on
if ((await this.waitForLeafReady(id)) === false) {
@@ -759,6 +774,7 @@ class LocalPouchDB {
}
async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> {
await this.waitForGCComplete();
let id = path2id(path);
try {
let obj: EntryDocResponse = null;
@@ -776,6 +792,10 @@ class LocalPouchDB {
// retrieve metadata only
if (!obj.type || (obj.type && obj.type == "notes") || obj.type == "newnote" || obj.type == "plain") {
let note = obj as Entry;
let children: string[] = [];
if (obj.type == "newnote" || obj.type == "plain") {
children = obj.children;
}
let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = {
data: "",
_id: note._id,
@@ -785,7 +805,7 @@ class LocalPouchDB {
_deleted: obj._deleted,
_rev: obj._rev,
_conflicts: obj._conflicts,
children: [],
children: children,
datatype: "newnote",
};
return doc;
@@ -798,7 +818,8 @@ class LocalPouchDB {
}
return false;
}
async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false): Promise<false | LoadedEntry> {
async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true): Promise<false | LoadedEntry> {
await this.waitForGCComplete();
let id = path2id(path);
try {
let obj: EntryDocResponse = null;
@@ -848,13 +869,14 @@ class LocalPouchDB {
}
let childrens: string[];
try {
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e)));
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady)));
if (dump) {
Logger(`childrens:`);
Logger(childrens);
}
} catch (ex) {
Logger(`Something went wrong on reading elements of ${obj._id} from database.`, LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.VERBOSE);
this.corruptedEntries[obj._id] = obj;
return false;
}
@@ -897,6 +919,7 @@ class LocalPouchDB {
return false;
}
async deleteDBEntry(path: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> {
await this.waitForGCComplete();
let id = path2id(path);
try {
let obj: EntryDocResponse = null;
@@ -939,6 +962,7 @@ class LocalPouchDB {
}
}
async deleteDBEntryPrefix(prefixSrc: string): Promise<boolean> {
await this.waitForGCComplete();
// delete database entries by prefix.
// it called from folder deletion.
let c = 0;
@@ -1000,6 +1024,7 @@ class LocalPouchDB {
return false;
}
async putDBEntry(note: LoadedEntry) {
await this.waitForGCComplete();
let leftData = note.data;
let savenNotes = [];
let processed = 0;
@@ -1161,7 +1186,7 @@ class LocalPouchDB {
} else {
Logger(`save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE);
Logger(item);
this.disposeHashCache();
// this.disposeHashCache();
saved = false;
}
}
@@ -1197,7 +1222,7 @@ class LocalPouchDB {
throw ex;
}
}
let r = await this.localDatabase.put(newDoc);
let r = await this.localDatabase.put(newDoc, { force: true });
this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted);
if (typeof this.corruptedEntries[note._id] != "undefined") {
delete this.corruptedEntries[note._id];
@@ -1223,6 +1248,7 @@ class LocalPouchDB {
}
replicateAllToServer(setting: ObsidianLiveSyncSettings, showingNotice?: boolean) {
return new Promise(async (res, rej) => {
await this.waitForGCComplete();
this.closeReplication();
Logger("send all data to server", LOG_LEVEL.NOTICE);
let notice: Notice = null;
@@ -1295,6 +1321,7 @@ class LocalPouchDB {
return false;
}
await this.waitForGCComplete();
if (setting.versionUpFlash != "") {
new Notice("Open settings and check message, please.");
return;
@@ -1471,11 +1498,13 @@ class LocalPouchDB {
}
async resetDatabase() {
await this.waitForGCComplete();
if (this.changeHandler != null) {
this.changeHandler.removeAllListeners();
this.changeHandler.cancel();
}
await this.closeReplication();
Logger("Database closed for reset Database.");
this.isReady = false;
await this.localDatabase.destroy();
this.localDatabase = null;
@@ -1576,72 +1605,87 @@ class LocalPouchDB {
Logger("Mark this device as 'resolved'.", LOG_LEVEL.NOTICE);
await dbret.db.put(remoteMilestone);
}
gcRunning = false;
async waitForGCComplete() {
while (this.gcRunning) {
Logger("Waiting for Garbage Collection completed.");
await delay(1000);
}
}
async garbageCollect() {
// get all documents of NewEntry2
// we don't use queries , just use allDocs();
let c = 0;
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: 500, conflicts: true });
readCount = result.rows.length;
Logger("checked:" + readCount);
if (readCount > 0) {
//there are some result
for (let v of result.rows) {
let doc = v.doc;
if (doc.type == "newnote" || doc.type == "plain") {
// used pieces memo.
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 (this.gcRunning) return;
this.gcRunning = true;
try {
// get all documents of NewEntry2
// we don't use queries , just use allDocs();
this.disposeHashCache();
let c = 0;
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: 500, conflicts: true });
readCount = result.rows.length;
Logger("checked:" + readCount);
if (readCount > 0) {
//there are some result
for (let v of result.rows) {
let doc = v.doc;
if (doc.type == "newnote" || doc.type == "plain") {
// used pieces memo.
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") {
// all pieces.
hashPieces = Array.from(new Set([...hashPieces, doc._id]));
}
}
if (doc.type == "leaf") {
// all pieces.
hashPieces = Array.from(new Set([...hashPieces, doc._id]));
}
c += readCount;
} while (readCount != 0);
// items collected.
Logger("Finding unused pieces");
this.disposeHashCache();
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;
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) {
// NO OP. It should be timing problem.
} else {
throw ex;
}
}
}
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;
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) {
// NO OP. It should be timing problem.
} else {
throw ex;
}
if (deleteDoc.length > 0) {
await this.localDatabase.bulkDocs(deleteDoc);
}
Logger(`GC:deleted ${deleteCount} items.`);
} finally {
this.gcRunning = false;
}
if (deleteDoc.length > 0) {
await this.localDatabase.bulkDocs(deleteDoc);
}
Logger(`GC:deleted ${deleteCount} items.`);
this.disposeHashCache();
}
}
@@ -1741,7 +1785,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
name: "Dump informations of this doc ",
editorCallback: (editor: Editor, view: MarkdownView) => {
//this.replicate();
this.localDatabase.getDBEntry(view.file.path, {}, true);
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
},
});
this.addCommand({
@@ -1993,6 +2037,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
addLogHook: () => void = null;
//--> Basic document Functions
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) {
return;
@@ -2010,8 +2055,32 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
// if (this.statusBar2 != null) {
// this.statusBar2.setText(newmessage.substring(0, 60));
// }
if (level >= LOG_LEVEL.NOTICE) {
new Notice(messagecontent);
if (messagecontent in this.notifies) {
clearTimeout(this.notifies[messagecontent].timer);
this.notifies[messagecontent].count++;
this.notifies[messagecontent].notice.setMessage(`(${this.notifies[messagecontent].count}):${messagecontent}`);
this.notifies[messagecontent].timer = setTimeout(() => {
const notify = this.notifies[messagecontent].notice;
delete this.notifies[messagecontent];
try {
notify.hide();
} catch (ex) {
// NO OP
}
}, 5000);
} else {
let notify = new Notice(messagecontent, 0);
this.notifies[messagecontent] = {
count: 0,
notice: notify,
timer: setTimeout(() => {
delete this.notifies[messagecontent];
notify.hide();
}, 5000),
};
}
}
if (this.addLogHook != null) this.addLogHook();
}
@@ -2182,10 +2251,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.refreshStatusText();
for (var change of docs) {
if (this.localDatabase.isSelfModified(change._id, change._rev)) {
return;
continue;
}
if (change._id.startsWith("ps:")) {
continue;
}
if (change._id.startsWith("h:")) {
continue;
}
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo" && change.type != "plugin") {
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
await this.handleDBChanged(change);
}
if (change.type == "versioninfo") {
@@ -2224,6 +2299,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
this.setPeriodicSync();
}
lastMessage = "";
refreshStatusText() {
let sent = this.localDatabase.docSent;
let arrived = this.localDatabase.docArrived;
@@ -2250,9 +2326,24 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.statusBar.title = this.localDatabase.syncStatus;
let waiting = "";
if (this.settings.batchSave) {
waiting = " " + this.batchFileChange.map((e) => "🚀").join("");
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
waiting = waiting.replace(/🛫{10}/g, "🚀");
}
const message = `Sync:${w}${sent}${arrived}${waiting}`;
this.setStatusBarText(message);
}
setStatusBarText(message: string) {
if (this.lastMessage != message) {
this.statusBar.setText(message);
if (this.settings.showStatusOnEditor) {
const root = document.documentElement;
root.style.setProperty("--slsmessage", '"' + message + '"');
} else {
const root = document.documentElement;
root.style.setProperty("--slsmessage", '""');
}
this.lastMessage = message;
}
this.statusBar.setText(`Sync:${w}${sent}${arrived}${waiting}`);
}
async replicate(showMessage?: boolean) {
if (this.settings.versionUpFlash != "") {
@@ -2298,7 +2389,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const syncFiles = filesStorage.filter((e) => onlyInStorageNames.indexOf(e.path) == -1);
Logger("Initialize and checking database files");
Logger("Updating database by new files");
this.statusBar.setText(`UPDATE DATABASE`);
this.setStatusBarText(`UPDATE DATABASE`);
let _this = this;
async function runAll<T>(procedurename: string, objects: T[], callback: (arg: T) => Promise<void>) {
const count = objects.length;
Logger(procedurename);
@@ -2315,7 +2407,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (notice != null) notice.setMessage(notify);
Logger(notify);
// lastTicks = performance.now() + 2000;
// this.statusBar.setText(notify);
_this.setStatusBarText(notify);
}
} catch (ex) {
Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE);
@@ -2346,12 +2438,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
});
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
Logger(`Pull from db:${e}`);
await this.pullFile(e, filesStorage);
await this.pullFile(e, filesStorage, false, null, false);
});
await runAll("CHECK FILE STATUS", syncFiles, async (e) => {
await this.syncFileBetweenDBandStorage(e, filesStorage);
});
this.statusBar.setText(`NOW TRACKING!`);
this.setStatusBarText(`NOW TRACKING!`);
Logger("Initialized,NOW TRACKING!");
if (showingNotice) {
notice.hide();
@@ -2547,20 +2639,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}).open();
}
}
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string) {
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady: boolean = true) {
if (!fileList) {
fileList = this.app.vault.getFiles();
}
let targetFiles = fileList.filter((e) => e.path == id2path(filename));
if (targetFiles.length == 0) {
//have to create;
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null);
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
if (doc === false) return;
await this.doc2storage_create(doc, force);
} else if (targetFiles.length == 1) {
//normal case
let file = targetFiles[0];
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null);
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
if (doc === false) return;
await this.doc2storate_modify(doc, file, force);
} else {
@@ -2572,18 +2664,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) {
let doc = await this.localDatabase.getDBEntryMeta(file.path);
if (doc === false) return;
let storageMtime = ~~(file.stat.mtime / 1000);
let docMtime = ~~(doc.mtime / 1000);
if (storageMtime > docMtime) {
//newer local file.
Logger("DB -> STORAGE :" + file.path);
Logger("STORAGE -> DB :" + file.path);
Logger(`${storageMtime} > ${docMtime}`);
await this.updateIntoDB(file);
} else if (storageMtime < docMtime) {
//newer database file.
Logger("STORAGE <- DB :" + file.path);
Logger(`${storageMtime} < ${docMtime}`);
let docx = await this.localDatabase.getDBEntry(file.path);
let docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
if (docx != false) {
await this.doc2storate_modify(docx, file);
}
@@ -2595,6 +2688,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
async updateIntoDB(file: TFile) {
await this.localDatabase.waitForGCComplete();
let content = "";
let datatype: "plain" | "newnote" = "newnote";
if (file.extension != "md") {
@@ -2616,7 +2710,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
datatype: datatype,
};
//From here
let old = await this.localDatabase.getDBEntry(fullpath);
let old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
if (old !== false) {
let oldData = { data: old.data, deleted: old._deleted };
let newData = { data: d.data, deleted: d._deleted };
@@ -2788,6 +2882,9 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." });
containerEl.createEl("h3", { text: "Remote Database configuration" });
let syncWarn = containerEl.createEl("div", { text: "The remote configuration is locked while any synchronization is enabled." });
syncWarn.addClass("op-warn");
syncWarn.addClass("sls-hidden");
const isAnySyncEnabled = (): boolean => {
if (this.plugin.settings.liveSync) return true;
@@ -2801,10 +2898,12 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
dbsettings.forEach((e) => {
e.setDisabled(true).setTooltip("When any sync is enabled, It cound't be changed.");
});
syncWarn.removeClass("sls-hidden");
} else {
dbsettings.forEach((e) => {
e.setDisabled(false).setTooltip("");
});
syncWarn.addClass("sls-hidden");
}
if (this.plugin.settings.liveSync) {
syncNonLive.forEach((e) => {
@@ -2996,6 +3095,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Apply and send")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-left")
.onClick(async () => {
await applyEncryption(true);
})
@@ -3005,6 +3105,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Apply and receive")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-right")
.onClick(async () => {
await applyEncryption(false);
})
@@ -3036,6 +3137,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
if (this.plugin.settings.versionUpFlash != "") {
let c = containerEl.createEl("div", { text: this.plugin.settings.versionUpFlash });
c.createEl("button", { text: "I got it and updated." }, (e) => {
e.addClass("mod-cta");
e.addEventListener("click", async () => {
this.plugin.settings.versionUpFlash = "";
await this.plugin.saveSettings();
@@ -3193,6 +3295,17 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
text.inputEl.setAttribute("type", "number");
});
containerEl.createEl("h3", { text: "Miscellaneous" });
new Setting(containerEl)
.setName("Show status inside editor")
.setDesc("")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showStatusOnEditor).onChange(async (value) => {
this.plugin.settings.showStatusOnEditor = value;
await this.plugin.saveSettings();
})
);
containerEl.createEl("h3", { text: "Hatch" });
if (this.plugin.localDatabase.remoteLockedAndDeviceNotAccepted) {
@@ -3200,6 +3313,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. ",
});
c.createEl("button", { text: "I'm ready, mark this device 'resolved'" }, (e) => {
e.addClass("mod-warning");
e.addEventListener("click", async () => {
await this.plugin.markRemoteResolved();
c.remove();
@@ -3212,6 +3326,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database.",
});
c.createEl("button", { text: "I'm ready, unlock the database" }, (e) => {
e.addClass("mod-warning");
e.addEventListener("click", async () => {
await this.plugin.markRemoteUnlocked();
c.remove();
@@ -3241,6 +3356,35 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.replicate(true);
}
};
new Setting(containerEl)
.setName("Reread all files")
.setDesc("Reread all files and update the database without dropping history")
.addButton((button) =>
button
.setButtonText("Reread")
.setDisabled(false)
.setWarning()
.onClick(async () => {
const files = this.app.vault.getFiles();
Logger("Reread all files started", LOG_LEVEL.NOTICE);
let notice = new Notice("", 0);
let i = 0;
for (const file of files) {
i++;
Logger(`Update into ${file.path}`);
notice.setMessage(`${i}/${files.length}\n${file.path}`);
try {
await this.plugin.updateIntoDB(file);
} catch (ex) {
Logger("could not update:");
Logger(ex);
}
}
notice.hide();
Logger("done", LOG_LEVEL.NOTICE);
})
);
new Setting(containerEl)
.setName("Drop History")
.setDesc("Initialize local and remote database, and send all or retrieve all again.")
@@ -3249,6 +3393,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Drop and send")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-left")
.onClick(async () => {
await dropHistory(true);
})
@@ -3258,6 +3403,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Drop and receive")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-right")
.onClick(async () => {
await dropHistory(false);
})
@@ -3270,6 +3416,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
button
.setButtonText("Lock")
.setDisabled(false)
.setWarning()
.onClick(async () => {
await this.plugin.markRemoteLocked();
})
@@ -3292,6 +3439,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
button
.setButtonText("Reset")
.setDisabled(false)
.setWarning()
.onClick(async () => {
await this.plugin.tryResetRemoteDatabase();
})
@@ -3303,6 +3451,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
button
.setButtonText("Reset")
.setDisabled(false)
.setWarning()
.onClick(async () => {
await this.plugin.resetLocalDatabase();
})
@@ -3335,6 +3484,17 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
// })
// );
new Setting(containerEl)
.setName("Show own plugins and settings")
.setDesc("Show ")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showOwnPlugins).onChange(async (value) => {
this.plugin.settings.showOwnPlugins = value;
await this.plugin.saveSettings();
updatePluginPane();
})
);
new Setting(containerEl)
.setName("Device and Vault name")
.setDesc("")
@@ -3363,7 +3523,6 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
// @ts-ignore
const pl = this.plugin.app.plugins;
const manifests: PluginManifest[] = Object.values(pl.manifests);
console.dir(manifests);
for (let m of manifests) {
let path = normalizePath(m.dir) + "/";
const adapter = this.plugin.app.vault.adapter;
@@ -3372,19 +3531,16 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
for (let file of files) {
let thePath = path + file;
if (await adapter.exists(thePath)) {
// pluginData[file] = await arrayBufferToBase64(await adapter.readBinary(thePath));
pluginData[file] = await adapter.read(thePath);
}
}
console.dir(m.id);
console.dir(pluginData);
let mtime = 0;
if (await adapter.exists(path + "/data.json")) {
mtime = (await adapter.stat(path + "/data.json")).mtime;
}
let p: PluginDataEntry = {
_id: `ps:${this.plugin.settings.deviceAndVaultName}-${m.id}`,
dataJson: pluginData["data.json"] ? await encrypt(pluginData["data.json"], this.plugin.settings.passphrase) : undefined,
dataJson: pluginData["data.json"],
deviceVaultName: this.plugin.settings.deviceAndVaultName,
mainJs: pluginData["main.js"],
styleCss: pluginData["style.css"],
@@ -3393,55 +3549,67 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
mtime: mtime,
type: "plugin",
};
await db.put(p);
let d: LoadedEntry = {
_id: p._id,
data: JSON.stringify(p),
ctime: mtime,
mtime: mtime,
size: 0,
children: [],
datatype: "plain",
};
await this.plugin.localDatabase.putDBEntry(d);
}
await this.plugin.replicate(true);
updatePluginPane();
};
const updatePluginPane = async () => {
const db = this.plugin.localDatabase.localDatabase;
let oldDocs = await db.allDocs<PluginDataEntry>({ startkey: `ps:`, endkey: `ps;`, include_docs: true });
let docList = await db.allDocs<PluginDataEntry>({ startkey: `ps:`, endkey: `ps;`, include_docs: false });
let oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.plugin.localDatabase.getDBEntry(e.id)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(e.data));
let plugins: { [key: string]: PluginDataEntry[] } = {};
let allPlugins: { [key: string]: PluginDataEntry } = {};
let thisDevicePlugins: { [key: string]: PluginDataEntry } = {};
for (let v of oldDocs.rows) {
if (typeof plugins[v.doc.deviceVaultName] === "undefined") {
plugins[v.doc.deviceVaultName] = [];
for (let v of oldDocs) {
if (typeof plugins[v.deviceVaultName] === "undefined") {
plugins[v.deviceVaultName] = [];
}
plugins[v.doc.deviceVaultName].push(v.doc);
allPlugins[v.doc._id] = v.doc;
if (v.doc.deviceVaultName == this.plugin.settings.deviceAndVaultName) {
thisDevicePlugins[v.doc.manifest.id] = v.doc;
plugins[v.deviceVaultName].push(v);
allPlugins[v._id] = v;
if (v.deviceVaultName == this.plugin.settings.deviceAndVaultName) {
thisDevicePlugins[v.manifest.id] = v;
}
}
let html = `
<table>
<tr>
<th>vault</th>
<th>plugin</th>
<th>version</th>
<th>modified</th>
<th>plugin</th>
<th>setting</th>
</tr>`;
<div class='sls-plugins-wrap'>
<table class='sls-plugins-tbl'>
`;
for (let vaults in plugins) {
if (vaults == this.plugin.settings.deviceAndVaultName) continue;
if (!this.plugin.settings.showOwnPlugins && vaults == this.plugin.settings.deviceAndVaultName) continue;
html += `
<tr>
<th colspan=2>${escapeStringToHTML(vaults)}</th>
</tr>`;
for (let v of plugins[vaults]) {
let mtime = v.mtime == 0 ? "-" : new Date(v.mtime).toLocaleString();
let settingApplyable: boolean | string = "-";
let settingFleshness: string = "";
let isSameVersion = false;
let isSameContents = false;
if (thisDevicePlugins[v.manifest.id]) {
if (thisDevicePlugins[v.manifest.id].manifest.version == v.manifest.version) {
isSameVersion = true;
}
if (thisDevicePlugins[v.manifest.id].styleCss == v.styleCss && thisDevicePlugins[v.manifest.id].mainJs == v.mainJs && thisDevicePlugins[v.manifest.id].manifestJson == v.manifestJson) {
isSameContents = true;
}
}
if (thisDevicePlugins[v.manifest.id] && thisDevicePlugins[v.manifest.id].dataJson && v.dataJson) {
// have this plugin.
let localSetting = await decrypt(thisDevicePlugins[v.manifest.id].dataJson, this.plugin.settings.passphrase);
let localSetting = thisDevicePlugins[v.manifest.id].dataJson;
try {
let remoteSetting = await decrypt(v.dataJson, this.plugin.settings.passphrase);
let remoteSetting = v.dataJson;
if (localSetting == remoteSetting) {
settingApplyable = "even";
} else {
@@ -3459,18 +3627,28 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
settingApplyable = "N/A";
}
// very ugly way.
let piece = `<tr>
<th>${escapeStringToHTML(v.deviceVaultName)}</th>
<td>${escapeStringToHTML(v.manifest.name)}</td>
<td class="tcenter">${escapeStringToHTML(v.manifest.version)}</td>
<td class="tcenter">${escapeStringToHTML(mtime)}</td>
<td class="tcenter">${isSameVersion ? "even" : "<button data-key='" + v._id + "' class='apply-plugin-version'>Use</button>"}</td>
<td class="tcenter">${settingApplyable === true ? "<button data-key='" + v._id + "' class='apply-plugin-data'>Apply (" + settingFleshness + ")</button>" : settingApplyable}</td>
</tr>`;
let piece = `
<tr class='divider'>
<th colspan=2></th>
</tr>
<tr>
<th class='sls-table-head'>${escapeStringToHTML(v.manifest.name)}</th>
<td class="sls-table-tail tcenter">${isSameContents ? "even" : `<button data-key='${v._id}' class='apply-plugin-version mod-cta'>Use (${isSameVersion ? "=" : ""}${v.manifest.version}) </button>`}</td>
</tr>
<tr>
<td class="sls-table-head tcenter">${escapeStringToHTML(mtime)}</td>
<td class="sls-table-tail tcenter">${settingApplyable === true ? "<button data-key='" + v._id + "' class='apply-plugin-data mod-cta'>Apply (" + settingFleshness + ")</button>" : settingApplyable}</td>
</tr>
`;
html += piece;
}
html += `
<tr class='divider'>
<th colspan=2></th>
</tr>
`;
}
html += "</table>";
html += "</table></div>";
pluginConfig.innerHTML = html;
pluginConfig.querySelectorAll(".apply-plugin-data").forEach((e) =>
e.addEventListener("click", async (evt) => {
@@ -3485,7 +3663,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.plugin.app.plugins.unloadPlugin(plugin.manifest.id);
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
}
if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", await decrypt(plugin.dataJson, this.plugin.settings.passphrase));
if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson);
Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL.NOTICE);
// @ts-ignore
if (stat) {
@@ -3518,7 +3696,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await adapter.write(pluginTargetFolderPath + "main.js", plugin.mainJs);
await adapter.write(pluginTargetFolderPath + "manifest.json", plugin.manifestJson);
if (plugin.styleCss) await adapter.write(pluginTargetFolderPath + "styles.css", plugin.styleCss);
if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", await decrypt(plugin.dataJson, this.plugin.settings.passphrase));
if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson);
if (stat) {
// @ts-ignore
await this.plugin.app.plugins.loadPlugin(plugin.manifest.id);
@@ -3550,6 +3728,14 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.setButtonText("Save plugins")
.setDisabled(false)
.onClick(async () => {
if (!this.plugin.settings.encrypt) {
Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE);
return;
}
if (!this.plugin.settings.deviceAndVaultName) {
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
return;
}
await sweepPlugin();
})
);
@@ -3567,6 +3753,19 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
xx.remove();
});
});
ba.addClass("mod-warning");
xx.createEl("button", { text: `Restore from file` }, (e) => {
e.addEventListener("click", async () => {
let f = await this.app.vault.getFiles().filter((e) => path2id(e.path) == k);
if (f.length == 0) {
Logger("Not found in vault", LOG_LEVEL.NOTICE);
return;
}
await this.plugin.updateIntoDB(f[0]);
xx.remove();
});
});
xx.addClass("mod-warning");
}
} 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.20",
"version": "0.1.24",
"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.20",
"version": "0.1.24",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.1.20",
"version": "0.1.24",
"license": "MIT",
"dependencies": {
"diff-match-patch": "^1.0.5",

View File

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

View File

@@ -31,3 +31,55 @@
.tcenter {
text-align: center;
}
.sls-plugins-wrap {
display: flex;
flex-grow: 1;
/* overflow: scroll; */
}
.sls-plugins-tbl {
border: 1px solid var(--background-modifier-border);
width: 100%;
}
.divider th {
border-top: 1px solid var(--background-modifier-border);
}
/* .sls-table-head{
width:50%;
}
.sls-table-tail{
width:50%;
} */
.sls-btn-left {
padding-right: 4px;
}
.sls-btn-right {
padding-left: 4px;
}
.sls-hidden {
display: none;
}
:root {
--slsmessage: "";
}
.CodeMirror-wrap::before , .cm-s-obsidian > .cm-editor::before {
content: var(--slsmessage);
position: absolute;
border-radius: 4px;
/* border:1px solid --background-modifier-border; */
display: inline-block;
top: 8px;
color: --text-normal;
opacity: 0.5;
font-size:80%;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.CodeMirror-wrap::before {
right: 0px;
} .cm-s-obsidian > .cm-editor::before {
right: 16px;
}