Compare commits

...

4 Commits

Author SHA1 Message Date
vorotamoroz
12753262fd bumped 2021-11-25 12:32:35 +09:00
vorotamoroz
97b34cff47 Fixed:
- the issue of filenames.

Improved:
- logging (error detail are now logged.)
- Testing remote DB.
2021-11-25 03:13:08 +09:00
vorotamoroz
85e29b99b2 Bumped and documented. 2021-11-24 17:37:13 +09:00
vorotamoroz
2d223a1439 Add new configuration
- Use newer file if conflicted
I mprove:.
- status message improved.
- fixed misconfigurations on automatically disabled sync.
2021-11-24 17:31:03 +09:00
6 changed files with 111 additions and 62 deletions

View File

@@ -47,7 +47,7 @@ If you are an early adopter, maybe this value is left as 30 seconds. Please chan
### Manual Garbage Collect ### Manual Garbage Collect
Run "Garbage Collection" manually. Run "Garbage Collection" manually.
### End to End Encryption (beta) ### End to End Encryption
Encrypt your database. It affects only the database, your files are left as plain. Encrypt your database. It affects only the database, your files are left as plain.
The encryption algorithm is AES-GCM. The encryption algorithm is AES-GCM.
@@ -119,6 +119,9 @@ If this option is enabled, move deleted files into the trash instead delete actu
### Do not delete empty folder ### Do not delete empty folder
Self-hosted LiveSync will delete the folder when the folder becomes empty. If this option is enabled, leave it as an empty folder. Self-hosted LiveSync will delete the folder when the folder becomes empty. If this option is enabled, leave it as an empty folder.
### Use newer file if conflicted (beta)
Always use the newer file to resolve and overwrite when conflict has occurred.
### minimum chunk size and LongLine threshold ### minimum chunk size and LongLine threshold
The configuration of chunk splitting. The configuration of chunk splitting.

View File

@@ -49,7 +49,7 @@ Obsidianでのファイル操作が終わってから指定秒数が経過した
### Manual Garbage Collect ### Manual Garbage Collect
上記のGarbage Collectionを手動で行います。 上記のGarbage Collectionを手動で行います。
### End to End Encryption (beta) ### End to End Encryption
データベースを暗号化します。この効果はデータベースに格納されるデータに限られ、ディスク上のファイルは平文のままです。 データベースを暗号化します。この効果はデータベースに格納されるデータに限られ、ディスク上のファイルは平文のままです。
暗号化はAES-GCMを使用して行っています。 暗号化はAES-GCMを使用して行っています。
@@ -121,6 +121,9 @@ LiveSyncをONにするか、もしくはPeriodic Sync + Sync On File Openがオ
Self-hosted LiveSyncは通常、フォルダ内のファイルがすべて削除された場合、フォルダを削除します。 Self-hosted LiveSyncは通常、フォルダ内のファイルがすべて削除された場合、フォルダを削除します。
備考:Self-hosted LiveSyncの同期対象はファイルです。 備考:Self-hosted LiveSyncの同期対象はファイルです。
### Use newer file if conflicted (beta)
競合が発生したとき、常に新しいファイルを使用して競合を自動的に解決します。
### minimum chunk size と LongLine threshold ### minimum chunk size と LongLine threshold
チャンクの分割についての設定です。 チャンクの分割についての設定です。
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ートを分割してチャンクを作成します。 Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ートを分割してチャンクを作成します。

155
main.ts
View File

@@ -264,8 +264,8 @@ const isValidRemoteCouchDBURI = (uri: string): boolean => {
if (uri.startsWith("http://")) return true; if (uri.startsWith("http://")) return true;
return false; return false;
}; };
const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }): Promise<false | { db: PouchDB.Database; info: any }> => { const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }): Promise<string | { db: PouchDB.Database; info: any }> => {
if (!isValidRemoteCouchDBURI(uri)) return false; if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
let db = new PouchDB(uri, { let db = new PouchDB(uri, {
auth, auth,
}); });
@@ -273,8 +273,12 @@ const connectRemoteCouchDB = async (uri: string, auth: { username: string; passw
let info = await db.info(); let info = await db.info();
return { db: db, info: info }; return { db: db, info: info };
} catch (ex) { } catch (ex) {
let msg = `${ex.name}:${ex.message}`;
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
}
Logger(ex, LOG_LEVEL.VERBOSE); Logger(ex, LOG_LEVEL.VERBOSE);
return false; return msg;
} }
}; };
// check the version of remote. // check the version of remote.
@@ -324,6 +328,7 @@ const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): P
await db.put(vi); await db.put(vi);
return true; return true;
}; };
function isValidPath(filename: string): boolean { function isValidPath(filename: string): boolean {
let regex = /[\u0000-\u001f]|[\\"':?<>|*$]/g; let regex = /[\u0000-\u001f]|[\\"':?<>|*$]/g;
let x = filename.replace(regex, "_"); let x = filename.replace(regex, "_");
@@ -332,10 +337,22 @@ function isValidPath(filename: string): boolean {
return sx == filename; return sx == filename;
} }
// For backward compatibility, using the path for determining id.
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
// The first slash will be deleted when the path is normalized.
function path2id(filename: string): string {
let x = normalizePath(filename);
if (x.startsWith("_")) x = "/" + x;
return x;
}
function id2path(filename: string): string {
return normalizePath(filename);
}
// Default Logger. // Default Logger.
let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message, _) => { let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message, _) => {
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 : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
let newmessage = timestamp + "->" + messagecontent; let newmessage = timestamp + "->" + messagecontent;
console.log(newmessage); console.log(newmessage);
}; };
@@ -720,7 +737,8 @@ class LocalPouchDB {
} }
} }
async getDBEntryMeta(id: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> { async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> {
let id = path2id(path);
try { try {
let obj: EntryDocResponse = null; let obj: EntryDocResponse = null;
if (opt) { if (opt) {
@@ -759,7 +777,8 @@ class LocalPouchDB {
} }
return false; return false;
} }
async getDBEntry(id: string, opt?: PouchDB.Core.GetOptions, dump = false): Promise<false | LoadedEntry> { async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false): Promise<false | LoadedEntry> {
let id = path2id(path);
try { try {
let obj: EntryDocResponse = null; let obj: EntryDocResponse = null;
if (opt) { if (opt) {
@@ -856,7 +875,8 @@ class LocalPouchDB {
} }
return false; return false;
} }
async deleteDBEntry(id: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> { async deleteDBEntry(path: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> {
let id = path2id(path);
try { try {
let obj: EntryDocResponse = null; let obj: EntryDocResponse = null;
if (opt) { if (opt) {
@@ -897,12 +917,13 @@ class LocalPouchDB {
throw ex; throw ex;
} }
} }
async deleteDBEntryPrefix(prefix: string): Promise<boolean> { async deleteDBEntryPrefix(prefixSrc: string): Promise<boolean> {
// delete database entries by prefix. // delete database entries by prefix.
// it called from folder deletion. // it called from folder deletion.
let c = 0; let c = 0;
let readCount = 0; let readCount = 0;
let delDocs: string[] = []; let delDocs: string[] = [];
let prefix = path2id(prefixSrc);
do { do {
let result = await this.localDatabase.allDocs({ include_docs: false, skip: c, limit: 100, conflicts: true }); let result = await this.localDatabase.allDocs({ include_docs: false, skip: c, limit: 100, conflicts: true });
readCount = result.rows.length; readCount = result.rows.length;
@@ -1195,10 +1216,10 @@ class LocalPouchDB {
password: setting.couchDB_PASSWORD, password: setting.couchDB_PASSWORD,
}; };
let dbret = await connectRemoteCouchDB(uri, auth); let dbret = await connectRemoteCouchDB(uri, auth);
if (dbret === false) { if (typeof dbret === "string") {
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE); Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
if (notice != null) notice.hide(); if (notice != null) notice.hide();
return rej(`could not connect to ${uri}`); return rej(`could not connect to ${uri}:${dbret}`);
} }
let syncOptionBase: PouchDB.Replication.SyncOptions = { let syncOptionBase: PouchDB.Replication.SyncOptions = {
@@ -1267,8 +1288,8 @@ class LocalPouchDB {
return false; return false;
} }
let dbret = await connectRemoteCouchDB(uri, auth); let dbret = await connectRemoteCouchDB(uri, auth);
if (dbret === false) { if (typeof dbret === "string") {
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE); Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
return; return;
} }
@@ -1449,7 +1470,7 @@ class LocalPouchDB {
password: setting.couchDB_PASSWORD, password: setting.couchDB_PASSWORD,
}; };
let con = await connectRemoteCouchDB(uri, auth); let con = await connectRemoteCouchDB(uri, auth);
if (con === false) return; if (typeof con == "string") return;
try { try {
await con.db.destroy(); await con.db.destroy();
Logger("Remote Database Destroyed", LOG_LEVEL.NOTICE); Logger("Remote Database Destroyed", LOG_LEVEL.NOTICE);
@@ -1466,7 +1487,7 @@ class LocalPouchDB {
password: setting.couchDB_PASSWORD, password: setting.couchDB_PASSWORD,
}; };
let con2 = await connectRemoteCouchDB(uri, auth); let con2 = await connectRemoteCouchDB(uri, auth);
if (con2 === false) return; if (typeof con2 === "string") return;
Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE); Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
} }
async markRemoteLocked(setting: ObsidianLiveSyncSettings, locked: boolean) { async markRemoteLocked(setting: ObsidianLiveSyncSettings, locked: boolean) {
@@ -1476,8 +1497,8 @@ class LocalPouchDB {
password: setting.couchDB_PASSWORD, password: setting.couchDB_PASSWORD,
}; };
let dbret = await connectRemoteCouchDB(uri, auth); let dbret = await connectRemoteCouchDB(uri, auth);
if (dbret === false) { if (typeof dbret === "string") {
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE); Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
return; return;
} }
@@ -1510,8 +1531,8 @@ class LocalPouchDB {
password: setting.couchDB_PASSWORD, password: setting.couchDB_PASSWORD,
}; };
let dbret = await connectRemoteCouchDB(uri, auth); let dbret = await connectRemoteCouchDB(uri, auth);
if (dbret === false) { if (typeof dbret === "string") {
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE); Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
return; return;
} }
@@ -1621,6 +1642,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.settings.liveSync = false; this.settings.liveSync = false;
this.settings.syncOnSave = false; this.settings.syncOnSave = false;
this.settings.syncOnStart = false; this.settings.syncOnStart = false;
this.settings.syncOnFileOpen = false;
this.settings.periodicReplication = 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.settings.versionUpFlash = "I changed specifications incompatiblly, so when you enable sync again, be sure to made version up all nother devides.";
this.saveSettings(); this.saveSettings();
@@ -1850,6 +1872,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
// If batchsave is enabled, queue all changes and do nothing. // If batchsave is enabled, queue all changes and do nothing.
if (this.settings.batchSave) { if (this.settings.batchSave) {
this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path])); this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path]));
this.refreshStatusText();
return; return;
} }
this.watchVaultChangeAsync(file, ...args); this.watchVaultChangeAsync(file, ...args);
@@ -1872,6 +1895,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger(ex, LOG_LEVEL.VERBOSE); Logger(ex, LOG_LEVEL.VERBOSE);
} }
}); });
this.refreshStatusText();
return Promise.all(promises); return Promise.all(promises);
} }
batchFileChange: string[] = []; batchFileChange: string[] = [];
@@ -1957,7 +1981,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
let valutName = this.app.vault.getName(); 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 : message instanceof Error ? `${message.name}:${message.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);
@@ -1993,37 +2017,39 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async doc2storage_create(docEntry: EntryBody, force?: boolean) { async doc2storage_create(docEntry: EntryBody, force?: boolean) {
let doc = await this.localDatabase.getDBEntry(docEntry._id, { rev: docEntry._rev }); let pathSrc = id2path(docEntry._id);
let doc = await this.localDatabase.getDBEntry(pathSrc, { rev: docEntry._rev });
if (doc === false) return; if (doc === false) return;
let path = id2path(doc._id);
if (doc.datatype == "newnote") { if (doc.datatype == "newnote") {
let bin = base64ToArrayBuffer(doc.data); let bin = base64ToArrayBuffer(doc.data);
if (bin != null) { if (bin != null) {
if (!isValidPath(doc._id)) { if (!isValidPath(path)) {
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${doc._id}`, LOG_LEVEL.NOTICE); Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE);
return; return;
} }
await this.ensureDirectory(doc._id); await this.ensureDirectory(path);
try { try {
let newfile = await this.app.vault.createBinary(normalizePath(doc._id), bin, { ctime: doc.ctime, mtime: doc.mtime }); let newfile = await this.app.vault.createBinary(normalizePath(path), bin, { ctime: doc.ctime, mtime: doc.mtime });
Logger("live : write to local (newfile:b) " + doc._id); Logger("live : write to local (newfile:b) " + path);
await this.app.vault.trigger("create", newfile); await this.app.vault.trigger("create", newfile);
} catch (ex) { } catch (ex) {
Logger("could not write to local (newfile:bin) " + doc._id, LOG_LEVEL.NOTICE); Logger("could not write to local (newfile:bin) " + path, LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.VERBOSE); Logger(ex, LOG_LEVEL.VERBOSE);
} }
} }
} else if (doc.datatype == "plain") { } else if (doc.datatype == "plain") {
if (!isValidPath(doc._id)) { if (!isValidPath(path)) {
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${doc._id}`, LOG_LEVEL.NOTICE); Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE);
return; return;
} }
await this.ensureDirectory(doc._id); await this.ensureDirectory(path);
try { try {
let newfile = await this.app.vault.create(normalizePath(doc._id), doc.data, { ctime: doc.ctime, mtime: doc.mtime }); let newfile = await this.app.vault.create(normalizePath(path), doc.data, { ctime: doc.ctime, mtime: doc.mtime });
Logger("live : write to local (newfile:p) " + doc._id); Logger("live : write to local (newfile:p) " + path);
await this.app.vault.trigger("create", newfile); await this.app.vault.trigger("create", newfile);
} catch (ex) { } catch (ex) {
Logger("could not write to local (newfile:plain) " + doc._id, LOG_LEVEL.NOTICE); Logger("could not write to local (newfile:plain) " + path, LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.VERBOSE); Logger(ex, LOG_LEVEL.VERBOSE);
} }
} else { } else {
@@ -2048,16 +2074,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) { async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
let pathSrc = id2path(docEntry._id);
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.getDBEntry(docEntry._id); let lastDocs = await this.localDatabase.getDBEntry(pathSrc);
if (lastDocs === false) { if (lastDocs === false) {
await this.deleteVaultItem(file); await this.deleteVaultItem(file);
} else { } else {
// it perhaps delete some revisions. // it perhaps delete some revisions.
// may be we have to reload this // may be we have to reload this
await this.pullFile(docEntry._id, null, true); await this.pullFile(pathSrc, null, true);
Logger(`delete skipped:${lastDocs._id}`); Logger(`delete skipped:${lastDocs._id}`);
} }
return; return;
@@ -2065,38 +2092,39 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
let localMtime = ~~(file.stat.mtime / 1000); let localMtime = ~~(file.stat.mtime / 1000);
let docMtime = ~~(docEntry.mtime / 1000); let docMtime = ~~(docEntry.mtime / 1000);
if (localMtime < docMtime || force) { if (localMtime < docMtime || force) {
let doc = await this.localDatabase.getDBEntry(docEntry._id); let doc = await this.localDatabase.getDBEntry(pathSrc);
let msg = "livesync : newer local files so write to local:" + file.path; let msg = "livesync : newer local files so write to local:" + file.path;
if (force) msg = "livesync : force write to local:" + file.path; if (force) msg = "livesync : force write to local:" + file.path;
if (doc === false) return; if (doc === false) return;
let path = id2path(doc._id);
if (doc.datatype == "newnote") { if (doc.datatype == "newnote") {
let bin = base64ToArrayBuffer(doc.data); let bin = base64ToArrayBuffer(doc.data);
if (bin != null) { if (bin != null) {
if (!isValidPath(doc._id)) { if (!isValidPath(path)) {
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${doc._id}`, LOG_LEVEL.NOTICE); Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE);
return; return;
} }
await this.ensureDirectory(doc._id); await this.ensureDirectory(path);
try { try {
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 });
Logger(msg); Logger(msg);
await this.app.vault.trigger("modify", file); await this.app.vault.trigger("modify", file);
} catch (ex) { } catch (ex) {
Logger("could not write to local (modify:bin) " + doc._id, LOG_LEVEL.NOTICE); Logger("could not write to local (modify:bin) " + path, LOG_LEVEL.NOTICE);
} }
} }
} else if (doc.datatype == "plain") { } else if (doc.datatype == "plain") {
if (!isValidPath(doc._id)) { if (!isValidPath(path)) {
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${doc._id}`, LOG_LEVEL.NOTICE); Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE);
return; return;
} }
await this.ensureDirectory(doc._id); await this.ensureDirectory(path);
try { try {
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 });
Logger(msg); Logger(msg);
await this.app.vault.trigger("modify", file); await this.app.vault.trigger("modify", file);
} catch (ex) { } catch (ex) {
Logger("could not write to local (modify:plain) " + doc._id, LOG_LEVEL.NOTICE); Logger("could not write to local (modify:plain) " + path, LOG_LEVEL.NOTICE);
} }
} else { } else {
Logger("live : New data imcoming, but we cound't parse that.:" + doc.datatype + "-", LOG_LEVEL.NOTICE); Logger("live : New data imcoming, but we cound't parse that.:" + doc.datatype + "-", LOG_LEVEL.NOTICE);
@@ -2111,7 +2139,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async handleDBChanged(change: EntryBody) { async handleDBChanged(change: EntryBody) {
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 == id2path(change._id));
if (targetFiles.length == 0) { if (targetFiles.length == 0) {
if (change._deleted) { if (change._deleted) {
return; return;
@@ -2199,7 +2227,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
w = "?"; w = "?";
} }
this.statusBar.title = this.localDatabase.syncStatus; this.statusBar.title = this.localDatabase.syncStatus;
this.statusBar.setText(`Sync:${w}${sent}${arrived}`); let waiting = "";
if (this.settings.batchSave) {
waiting = " " + this.batchFileChange.map((e) => "🚀").join("");
}
this.statusBar.setText(`Sync:${w}${sent}${arrived}${waiting}`);
} }
async replicate(showMessage?: boolean) { async replicate(showMessage?: boolean) {
if (this.settings.versionUpFlash != "") { if (this.settings.versionUpFlash != "") {
@@ -2235,7 +2267,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const filesStorage = this.app.vault.getFiles(); const filesStorage = this.app.vault.getFiles();
const filesStorageName = filesStorage.map((e) => e.path); const filesStorageName = filesStorage.map((e) => e.path);
const wf = await this.localDatabase.localDatabase.allDocs(); const wf = await this.localDatabase.localDatabase.allDocs();
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && e.id != "obsydian_livesync_version").map((e) => normalizePath(e.id)); const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && e.id != "obsydian_livesync_version").map((e) => id2path(e.id));
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);
@@ -2250,18 +2282,18 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const count = objects.length; const count = objects.length;
Logger(procedurename); Logger(procedurename);
let i = 0; let i = 0;
let lastTicks = performance.now() + 2000; // let lastTicks = performance.now() + 2000;
let procs = objects.map(async (e) => { let procs = objects.map(async (e) => {
try { try {
// debugger; // debugger;
// Logger("hello?") // Logger("hello?")
await callback(e); await callback(e);
i++; i++;
if (lastTicks < performance.now()) { if (i % 25 == 0) {
const notify = `${procedurename} : ${i}/${count}`; const notify = `${procedurename} : ${i}/${count}`;
if (notice != null) notice.setMessage(notify); if (notice != null) notice.setMessage(notify);
Logger(notify); Logger(notify);
lastTicks = performance.now() + 2000; // lastTicks = performance.now() + 2000;
// this.statusBar.setText(notify); // this.statusBar.setText(notify);
} }
} catch (ex) { } catch (ex) {
@@ -2435,7 +2467,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev }); await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
await this.pullFile(path, null, true); await this.pullFile(path, null, true);
Logger(`automaticaly merged (newerFileResolve) :${path}`); Logger(`Automaticaly merged (newerFileResolve) :${path}`, LOG_LEVEL.NOTICE);
return true; return true;
} }
// make diff. // make diff.
@@ -2498,7 +2530,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (!fileList) { if (!fileList) {
fileList = this.app.vault.getFiles(); fileList = this.app.vault.getFiles();
} }
let targetFiles = fileList.filter((e) => e.path == normalizePath(filename)); let targetFiles = fileList.filter((e) => e.path == id2path(filename));
if (targetFiles.length == 0) { if (targetFiles.length == 0) {
//have to create; //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);
@@ -2552,7 +2584,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
content = await this.app.vault.read(file); content = await this.app.vault.read(file);
datatype = "plain"; datatype = "plain";
} }
let fullpath = file.path; let fullpath = path2id(file.path);
let d: LoadedEntry = { let d: LoadedEntry = {
_id: fullpath, _id: fullpath,
data: content, data: content,
@@ -2721,8 +2753,8 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
username: this.plugin.settings.couchDB_USER, username: this.plugin.settings.couchDB_USER,
password: this.plugin.settings.couchDB_PASSWORD, password: this.plugin.settings.couchDB_PASSWORD,
}); });
if (db === false) { if (typeof db === "string") {
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME}`, LOG_LEVEL.NOTICE); this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE);
return; return;
} }
this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL.NOTICE); this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL.NOTICE);
@@ -2877,7 +2909,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
); );
new Setting(containerEl) new Setting(containerEl)
.setName("End to End Encryption (beta)") .setName("End to End Encryption")
.setDesc("Encrypting contents on the database.") .setDesc("Encrypting contents on the database.")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => { toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => {
@@ -2918,6 +2950,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.periodicReplication = false; this.plugin.settings.periodicReplication = false;
this.plugin.settings.syncOnSave = false; this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnStart = false; this.plugin.settings.syncOnStart = false;
this.plugin.settings.syncOnFileOpen = false;
this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt; this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt;
this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase; this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase;
@@ -3096,6 +3129,15 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
); );
new Setting(containerEl)
.setName("Use newer file if conflicted (beta)")
.setDesc("Resolve conflicts by newer files automatically.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.resolveConflictsByNewerFile).onChange(async (value) => {
this.plugin.settings.resolveConflictsByNewerFile = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl) new Setting(containerEl)
.setName("Minimum chunk size") .setName("Minimum chunk size")
.setDesc("(letters), minimum chunk size.") .setDesc("(letters), minimum chunk size.")
@@ -3162,6 +3204,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.periodicReplication = false; this.plugin.settings.periodicReplication = false;
this.plugin.settings.syncOnSave = false; this.plugin.settings.syncOnSave = false;
this.plugin.settings.syncOnStart = false; this.plugin.settings.syncOnStart = false;
this.plugin.settings.syncOnFileOpen = false;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
applyDisplayEnabled(); applyDisplayEnabled();

View File

@@ -1,7 +1,7 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Self-hosted LiveSync", "name": "Self-hosted LiveSync",
"version": "0.1.17", "version": "0.1.19",
"minAppVersion": "0.9.12", "minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz", "author": "vorotamoroz",

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.1.16", "version": "0.1.19",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {