Compare commits

...

4 Commits

Author SHA1 Message Date
vorotamoroz
53b4d4cd20 fixes below:
Make text selectable in log dialog.
Dumping errors when couldn't connect
Invalid uri could not detected.
Add tooltips to statusbar
2021-11-08 17:52:07 +09:00
vorotamoroz
d324f08240 Renamed - very lucid! 2021-11-05 16:38:45 +09:00
vorotamoroz
0b526e9cea Fixed boot issue and Improved deletion
- fixed bootup delay when vault contains many files.
- status bar improvement.
- add new feature, using trash when file has been delete in remote.
2021-11-04 19:12:43 +09:00
vorotamoroz
07535eb3fc Update README.md 2021-10-31 12:53:44 +09:00
6 changed files with 234 additions and 58 deletions

View File

@@ -1,13 +1,15 @@
# obsidian-livesync # Self-hosted LiveSync
**Renamed from: obsidian-livesync**
This is the obsidian plugin that enables livesync between multi-devices. This is the obsidian plugin that enables livesync between multi-devices with self-hosted database.
Runs in Mac, Android, Windows, and iOS. Runs in Mac, Android, Windows, and iOS.
Community implementation, not compatible with official "Sync".
<!-- <div><video controls src="https://user-images.githubusercontent.com/45774780/137352386-a274736d-a38b-4069-ac41-759c73e36a23.mp4" muted="false"></video></div> --> <!-- <div><video controls src="https://user-images.githubusercontent.com/45774780/137352386-a274736d-a38b-4069-ac41-759c73e36a23.mp4" muted="false"></video></div> -->
![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif) ![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
**It's beta. Please make sure to back your vault up!** **It's getting almost stable now, But Please make sure to back your vault up!**
Limitations: Folder deletion handling is not completed. Limitations: Folder deletion handling is not completed.
@@ -26,7 +28,7 @@ If you want to synchronize to both backend, sync one by one, please.
## How to use ## How to use
1. Install from Obsidian, or clone this repo and run `npm run build` ,copy `main.js`, `styles.css` and `manifest.json` into `[your-vault]/.obsidian/plugins/` (PC, Mac and Android will work) 1. Install from Obsidian, or clone this repo and run `npm run build` ,copy `main.js`, `styles.css` and `manifest.json` into `[your-vault]/.obsidian/plugins/` (PC, Mac and Android will work)
2. Enable obsidian livesync in the settings dialog. 2. Enable Self-hosted LiveSync in the settings dialog.
3. If you use your self-hosted CouchDB, set your server's info. 3. If you use your self-hosted CouchDB, set your server's info.
4. or Use [IBM Cloudant](https://www.ibm.com/cloud/cloudant), take an account and enable **Cloudant** in [Catalog](https://cloud.ibm.com/catalog#services) 4. or Use [IBM Cloudant](https://www.ibm.com/cloud/cloudant), take an account and enable **Cloudant** in [Catalog](https://cloud.ibm.com/catalog#services)
Note please choose "IAM and legacy credentials" for the Authentication method Note please choose "IAM and legacy credentials" for the Authentication method
@@ -35,7 +37,7 @@ If you want to synchronize to both backend, sync one by one, please.
## Test Server ## Test Server
Setting up an instance of Cloudant or local CouchDB is a little complicated, so I made the [Tasting server of obsidian-livesync](https://olstaste.vrtmrz.net/) up. Try free! Setting up an instance of Cloudant or local CouchDB is a little complicated, so I made the [Tasting server of self-hosted-livesync](https://olstaste.vrtmrz.net/) up. Try free!
Note: Please read "Limitations" carefully. Do not send your private vault. Note: Please read "Limitations" carefully. Do not send your private vault.
## WebClipper is also available. ## WebClipper is also available.
@@ -45,7 +47,7 @@ Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-liv
## When your database looks corrupted or too heavy to replicate to a new device. ## When your database looks corrupted or too heavy to replicate to a new device.
obsidian-livesync changes data treatment of markdown files since 0.1.0 self-hosted-livesync changes data treatment of markdown files since 0.1.0
When you are troubled with synchronization, **Please reset local and remote databases**. When you are troubled with synchronization, **Please reset local and remote databases**.
_Note: Without synchronization, your files won't be deleted._ _Note: Without synchronization, your files won't be deleted._
@@ -53,7 +55,7 @@ _Note: Without synchronization, your files won't be deleted._
1. Disable any synchronizations on all devices. 1. Disable any synchronizations on all devices.
1. From the most reliable device<sup>(_The device_)</sup>, back your vault up. 1. From the most reliable device<sup>(_The device_)</sup>, back your vault up.
1. Press "Drop History"-> "Execute" button from _The device_. 1. Press "Drop History"-> "Execute" button from _The device_.
1. Wait for a while, so obsidian-livesync will say "completed." 1. Wait for a while, so self-hosted-livesync will say "completed."
1. In other devices, replication will be canceled automatically. Click "Reset local database" and click "I'm ready, mark this device 'resolved'" on all devices. 1. In other devices, replication will be canceled automatically. Click "Reset local database" and click "I'm ready, mark this device 'resolved'" on all devices.
If it doesn't be shown. replicate once. If it doesn't be shown. replicate once.
1. It's all done. But if you are sure to resolve all devices and the warning is noisy, click "I'm ready, unlock the database". it unlocks the database completely. 1. It's all done. But if you are sure to resolve all devices and the warning is noisy, click "I'm ready, unlock the database". it unlocks the database completely.
@@ -102,7 +104,7 @@ Select Multitenant(it's the default) and the region as you like.
6. When all of the above steps have been done, Open "Resource list" on the left pane. you can see the Cloudant instance in the "Service and software". Click it. 6. When all of the above steps have been done, Open "Resource list" on the left pane. you can see the Cloudant instance in the "Service and software". Click it.
![step 8](instruction_images/cloudant_8.png) ![step 8](instruction_images/cloudant_8.png)
7. In resource details, there's information to connect from obsidian-livesync. 7. In resource details, there's information to connect from self-hosted-livesync.
Copy the "External Endpoint(preferred)" address. <sup>(\*1)</sup> Copy the "External Endpoint(preferred)" address. <sup>(\*1)</sup>
![step 9](instruction_images/cloudant_9.png) ![step 9](instruction_images/cloudant_9.png)
@@ -133,7 +135,7 @@ Select Multitenant(it's the default) and the region as you like.
1. The dialog to create a credential will be shown. 1. The dialog to create a credential will be shown.
type any name or leave it default, hit the "Add" button. type any name or leave it default, hit the "Add" button.
![step 2](instruction_images/credentials_2.png) ![step 2](instruction_images/credentials_2.png)
_NOTE: This "name" is not related to your username that uses in Obsidian-livesync._ _NOTE: This "name" is not related to your username that uses in self-hosted-livesync._
1. Back to "Service credentials", the new credential should be created. 1. Back to "Service credentials", the new credential should be created.
open details. open details.
@@ -143,7 +145,7 @@ Select Multitenant(it's the default) and the region as you like.
follow the figure, it's follow the figure, it's
"apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5"<sup>(\*3)</sup> and "c2c11651d75497fa3d3c486e4c8bdf27"<sup>(\*4)</sup> "apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5"<sup>(\*3)</sup> and "c2c11651d75497fa3d3c486e4c8bdf27"<sup>(\*4)</sup>
### obsidian-livesync setting ### self-hosted-livesync setting
![xx](instruction_images/obsidian_sync_1.png) ![xx](instruction_images/obsidian_sync_1.png)
example values. example values.

245
main.ts
View File

@@ -1,4 +1,4 @@
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder } from "obsidian"; import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath } from "obsidian";
import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser"; import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import xxhash from "xxhash-wasm"; import xxhash from "xxhash-wasm";
@@ -39,6 +39,7 @@ interface ObsidianLiveSyncSettings {
longLineThreshold: number; longLineThreshold: number;
showVerboseLog: boolean; showVerboseLog: boolean;
suspendFileWatching: boolean; suspendFileWatching: boolean;
trashInsteadDelete: boolean;
} }
const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = { const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
@@ -56,6 +57,7 @@ const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
longLineThreshold: 250, longLineThreshold: 250,
showVerboseLog: false, showVerboseLog: false,
suspendFileWatching: false, suspendFileWatching: false,
trashInsteadDelete: false,
}; };
interface Entry { interface Entry {
_id: string; _id: string;
@@ -229,7 +231,7 @@ const isValidRemoteCouchDBURI = (uri: string): boolean => {
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<false | { db: PouchDB.Database; info: any }> => {
if (!isValidRemoteCouchDBURI(uri)) false; if (!isValidRemoteCouchDBURI(uri)) return false;
let db = new PouchDB(uri, { let db = new PouchDB(uri, {
auth, auth,
}); });
@@ -237,6 +239,7 @@ 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) {
Logger(ex, LOG_LEVEL.VERBOSE);
return false; return false;
} }
}; };
@@ -303,6 +306,8 @@ let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message
console.log(newmessage); console.log(newmessage);
}; };
type DatabaseConnectingStatus = "NOT_CONNECTED" | "PAUSED" | "CONNECTED" | "COMPLETED" | "CLOSED" | "ERRORED";
//<--Functions //<--Functions
class LocalPouchDB { class LocalPouchDB {
auth: Credential; auth: Credential;
@@ -397,6 +402,7 @@ class LocalPouchDB {
.on("change", (e) => { .on("change", (e) => {
if (e.deleted) return; if (e.deleted) return;
this.leafArrived(e.id); this.leafArrived(e.id);
this.docSeq = `${e.seq}`;
}); });
this.changeHandler = changes; this.changeHandler = changes;
await this.prepareHashFunctions(); await this.prepareHashFunctions();
@@ -479,6 +485,44 @@ class LocalPouchDB {
} }
} }
async getDBEntryMeta(id: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> {
try {
let obj: EntryDocResponse = null;
if (opt) {
obj = await this.localDatabase.get(id, opt);
} else {
obj = await this.localDatabase.get(id);
}
if (obj.type && obj.type == "leaf") {
//do nothing for leaf;
return false;
}
// retrieve metadata only
if (!obj.type || (obj.type && obj.type == "notes") || obj.type == "newnote" || obj.type == "plain") {
let note = obj as Entry;
let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = {
data: "",
_id: note._id,
ctime: note.ctime,
mtime: note.mtime,
size: note.size,
_deleted: obj._deleted,
_rev: obj._rev,
_conflicts: obj._conflicts,
children: [],
datatype: "newnote",
};
}
} catch (ex) {
if (ex.status && ex.status == 404) {
return false;
}
throw ex;
}
return false;
}
async getDBEntry(id: string, opt?: PouchDB.Core.GetOptions, retryCount = 5): Promise<false | LoadedEntry> { async getDBEntry(id: string, opt?: PouchDB.Core.GetOptions, retryCount = 5): Promise<false | LoadedEntry> {
try { try {
let obj: EntryDocResponse = null; let obj: EntryDocResponse = null;
@@ -809,7 +853,13 @@ class LocalPouchDB {
} }
syncHandler: PouchDB.Replication.Sync<{}> = null; syncHandler: PouchDB.Replication.Sync<{}> = null;
syncStatus: DatabaseConnectingStatus = "NOT_CONNECTED";
docArrived: number = 0;
docSent: number = 0;
docSeq: string = "";
updateInfo: () => void = () => {
console.log("default updinfo");
};
async migrate(from: number, to: number): Promise<boolean> { async migrate(from: number, to: number): Promise<boolean> {
Logger(`Database updated from ${from} to ${to}`, LOG_LEVEL.NOTICE); Logger(`Database updated from ${from} to ${to}`, LOG_LEVEL.NOTICE);
// no op now, // no op now,
@@ -819,6 +869,8 @@ class LocalPouchDB {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
this.closeReplication(); this.closeReplication();
Logger("send all data to server", LOG_LEVEL.NOTICE); Logger("send all data to server", LOG_LEVEL.NOTICE);
this.syncStatus = "CLOSED";
this.updateInfo();
let uri = setting.couchDB_URI; let uri = setting.couchDB_URI;
let auth: Credential = { let auth: Credential = {
username: setting.couchDB_USER, username: setting.couchDB_USER,
@@ -839,17 +891,28 @@ class LocalPouchDB {
//replicate once //replicate once
let replicate = this.localDatabase.replicate.to(db, syncOptionBase); let replicate = this.localDatabase.replicate.to(db, syncOptionBase);
replicate replicate
.on("active", () => {
this.syncStatus = "CONNECTED";
this.updateInfo();
})
.on("change", async (e) => { .on("change", async (e) => {
// no op. // no op.
this.docSent += e.docs_written;
this.docArrived += e.docs_read;
this.updateInfo();
Logger(`sending..:${e.docs.length}`); Logger(`sending..:${e.docs.length}`);
}) })
.on("complete", async (info) => { .on("complete", async (info) => {
this.syncStatus = "COMPLETED";
this.updateInfo();
Logger("Completed", LOG_LEVEL.NOTICE); Logger("Completed", LOG_LEVEL.NOTICE);
replicate.cancel(); replicate.cancel();
replicate.removeAllListeners(); replicate.removeAllListeners();
res(true); res(true);
}) })
.on("error", (e) => { .on("error", (e) => {
this.syncStatus = "ERRORED";
this.updateInfo();
Logger("Pulling Replication error", LOG_LEVEL.NOTICE); Logger("Pulling Replication error", LOG_LEVEL.NOTICE);
Logger(e); Logger(e);
rej(e); rej(e);
@@ -877,7 +940,7 @@ class LocalPouchDB {
} }
if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) {
Logger("Remote database is newer or corrupted, make sure to latest version of obsidian-livesync installed", LOG_LEVEL.NOTICE); Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE);
return; return;
} }
@@ -909,14 +972,22 @@ class LocalPouchDB {
let db = dbret.db; let db = dbret.db;
//replicate once //replicate once
this.syncStatus = "CONNECTED";
let replicate = this.localDatabase.replicate.from(db, syncOptionBase); let replicate = this.localDatabase.replicate.from(db, syncOptionBase);
replicate replicate
.on("active", () => {
this.syncStatus = "CONNECTED";
this.updateInfo();
})
.on("change", async (e) => { .on("change", async (e) => {
// when in first run, replication will send us tombstone data // when in first run, replication will send us tombstone data
// and in normal cases, all leavs should sent before the entry that contains these item. // and in normal cases, all leavs should sent before the entry that contains these item.
// so skip to completed all, we should treat all changes. // so skip to completed all, we should treat all changes.
try { try {
callback(e.docs); callback(e.docs);
this.docArrived += e.docs_read;
this.docSent += e.docs_written;
this.updateInfo();
Logger(`pulled ${e.docs.length} doc(s)`); Logger(`pulled ${e.docs.length} doc(s)`);
} catch (ex) { } catch (ex) {
Logger("Replication callback error"); Logger("Replication callback error");
@@ -924,6 +995,8 @@ class LocalPouchDB {
} }
}) })
.on("complete", async (info) => { .on("complete", async (info) => {
this.syncStatus = "COMPLETED";
this.updateInfo();
replicate.cancel(); replicate.cancel();
replicate.removeAllListeners(); replicate.removeAllListeners();
this.syncHandler = null; this.syncHandler = null;
@@ -934,10 +1007,15 @@ class LocalPouchDB {
this.syncHandler = this.localDatabase.sync(db, syncOption); this.syncHandler = this.localDatabase.sync(db, syncOption);
this.syncHandler this.syncHandler
.on("active", () => { .on("active", () => {
this.syncStatus = "CONNECTED";
this.updateInfo();
Logger("Replication activated"); Logger("Replication activated");
}) })
.on("change", async (e) => { .on("change", async (e) => {
try { try {
this.docArrived += e.change.docs_read;
this.docSent += e.change.docs_written;
this.updateInfo();
callback(e.change.docs); callback(e.change.docs);
Logger(`replicated ${e.change.docs.length} doc(s)`); Logger(`replicated ${e.change.docs.length} doc(s)`);
} catch (ex) { } catch (ex) {
@@ -946,23 +1024,33 @@ class LocalPouchDB {
} }
}) })
.on("complete", (e) => { .on("complete", (e) => {
this.syncStatus = "COMPLETED";
this.updateInfo();
Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO); Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
this.syncHandler = null; this.syncHandler = null;
}) })
.on("denied", (e) => { .on("denied", (e) => {
this.syncStatus = "ERRORED";
this.updateInfo();
Logger("Replication denied", LOG_LEVEL.NOTICE); Logger("Replication denied", LOG_LEVEL.NOTICE);
// Logger(e); // Logger(e);
}) })
.on("error", (e) => { .on("error", (e) => {
this.syncStatus = "ERRORED";
this.updateInfo();
Logger("Replication error", LOG_LEVEL.NOTICE); Logger("Replication error", LOG_LEVEL.NOTICE);
// Logger(e); // Logger(e);
}) })
.on("paused", (e) => { .on("paused", (e) => {
this.syncStatus = "PAUSED";
this.updateInfo();
Logger("replication paused", LOG_LEVEL.VERBOSE); Logger("replication paused", LOG_LEVEL.VERBOSE);
// Logger(e); // Logger(e);
}); });
}) })
.on("error", (e) => { .on("error", (e) => {
this.syncStatus = "ERRORED";
this.updateInfo();
Logger("Pulling Replication error", LOG_LEVEL.NOTICE); Logger("Pulling Replication error", LOG_LEVEL.NOTICE);
Logger(e); Logger(e);
}); });
@@ -972,6 +1060,8 @@ class LocalPouchDB {
if (this.syncHandler == null) { if (this.syncHandler == null) {
return; return;
} }
this.syncStatus = "CLOSED";
this.updateInfo();
this.syncHandler.cancel(); this.syncHandler.cancel();
this.syncHandler.removeAllListeners(); this.syncHandler.removeAllListeners();
this.syncHandler = null; this.syncHandler = null;
@@ -1031,7 +1121,7 @@ class LocalPouchDB {
} }
if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) {
Logger("Remote database is newer or corrupted, make sure to latest version of obsidian-livesync installed", LOG_LEVEL.NOTICE); Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE);
return; return;
} }
let defInitPoint: EntryMilestoneInfo = { let defInitPoint: EntryMilestoneInfo = {
@@ -1065,7 +1155,7 @@ class LocalPouchDB {
} }
if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) {
Logger("Remote database is newer or corrupted, make sure to latest version of obsidian-livesync installed", LOG_LEVEL.NOTICE); Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE);
return; return;
} }
let defInitPoint: EntryMilestoneInfo = { let defInitPoint: EntryMilestoneInfo = {
@@ -1187,6 +1277,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}); });
this.statusBar = this.addStatusBarItem(); this.statusBar = this.addStatusBarItem();
this.statusBar.addClass("syncstatusbar");
this.refreshStatusText = this.refreshStatusText.bind(this);
this.statusBar2 = this.addStatusBarItem(); this.statusBar2 = this.addStatusBarItem();
let delay = this.settings.savingDelay; let delay = this.settings.savingDelay;
@@ -1278,6 +1370,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
let vaultName = this.app.vault.getName(); let vaultName = this.app.vault.getName();
this.localDatabase = new LocalPouchDB(this.settings, vaultName); this.localDatabase = new LocalPouchDB(this.settings, vaultName);
this.localDatabase.updateInfo = () => {
this.refreshStatusText();
};
await this.localDatabase.initializeDatabase(); await this.localDatabase.initializeDatabase();
} }
async garbageCollect() { async garbageCollect() {
@@ -1366,8 +1461,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//--> Basic document Functions //--> Basic document Functions
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) { async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
// debugger;
if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) { if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) {
return; return;
} }
@@ -1381,9 +1474,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100); this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
console.log(valutName + ":" + newmessage); console.log(valutName + ":" + newmessage);
if (this.statusBar2 != null) { // if (this.statusBar2 != null) {
this.statusBar2.setText(newmessage.substring(0, 60)); // this.statusBar2.setText(newmessage.substring(0, 60));
} // }
if (level >= LOG_LEVEL.NOTICE) { if (level >= LOG_LEVEL.NOTICE) {
new Notice(messagecontent); new Notice(messagecontent);
} }
@@ -1421,9 +1514,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return; return;
} }
await this.ensureDirectory(doc._id); await this.ensureDirectory(doc._id);
let newfile = await this.app.vault.createBinary(doc._id, bin, { ctime: doc.ctime, mtime: doc.mtime }); try {
Logger("live : write to local (newfile:b) " + doc._id); let newfile = await this.app.vault.createBinary(normalizePath(doc._id), bin, { ctime: doc.ctime, mtime: doc.mtime });
await this.app.vault.trigger("create", newfile); Logger("live : write to local (newfile:b) " + doc._id);
await this.app.vault.trigger("create", newfile);
} catch (ex) {
Logger("could not write to local (newfile:bin) " + doc._id, LOG_LEVEL.NOTICE);
}
} }
} else if (doc.datatype == "plain") { } else if (doc.datatype == "plain") {
if (!isValidPath(doc._id)) { if (!isValidPath(doc._id)) {
@@ -1431,9 +1528,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return; return;
} }
await this.ensureDirectory(doc._id); await this.ensureDirectory(doc._id);
let newfile = await this.app.vault.create(doc._id, doc.data, { ctime: doc.ctime, mtime: doc.mtime }); try {
Logger("live : write to local (newfile:p) " + doc._id); let newfile = await this.app.vault.create(normalizePath(doc._id), doc.data, { ctime: doc.ctime, mtime: doc.mtime });
await this.app.vault.trigger("create", newfile); Logger("live : write to local (newfile:p) " + doc._id);
await this.app.vault.trigger("create", newfile);
} catch (ex) {
Logger("could not write to local (newfile:plain) " + doc._id, 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);
} }
@@ -1441,7 +1542,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async deleteVaultItem(file: TFile | TFolder) { async deleteVaultItem(file: TFile | TFolder) {
let dir = file.parent; let dir = file.parent;
await this.app.vault.delete(file); if (this.settings.trashInsteadDelete) {
await this.app.vault.trash(file, false);
} else {
await this.app.vault.delete(file);
}
Logger(`deleted:${file.path}`); Logger(`deleted:${file.path}`);
Logger(`other items:${dir.children.length}`); Logger(`other items:${dir.children.length}`);
if (dir.children.length == 0) { if (dir.children.length == 0) {
@@ -1478,19 +1583,28 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return; return;
} }
await this.ensureDirectory(doc._id); await this.ensureDirectory(doc._id);
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime }); try {
Logger(msg); await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
await this.app.vault.trigger("modify", file); Logger(msg);
await this.app.vault.trigger("modify", file);
} catch (ex) {
Logger("could not write to local (modify:bin) " + doc._id, LOG_LEVEL.NOTICE);
}
} }
} else if (doc.datatype == "plain") { }
if (doc.datatype == "plain") {
if (!isValidPath(doc._id)) { if (!isValidPath(doc._id)) {
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: ${doc._id}`, LOG_LEVEL.NOTICE);
return; return;
} }
await this.ensureDirectory(doc._id); await this.ensureDirectory(doc._id);
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime }); try {
Logger(msg); await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
await this.app.vault.trigger("modify", file); Logger(msg);
await this.app.vault.trigger("modify", file);
} catch (ex) {
Logger("could not write to local (modify:plain) " + doc._id, 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);
} }
@@ -1522,6 +1636,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//---> Sync //---> Sync
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> { async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
this.refreshStatusText();
for (var change of docs) { for (var change of docs) {
if (this.localDatabase.isSelfModified(change._id, change._rev)) { if (this.localDatabase.isSelfModified(change._id, change._rev)) {
return; return;
@@ -1533,13 +1648,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (change.type == "versioninfo") { if (change.type == "versioninfo") {
if (change.version > VER) { if (change.version > VER) {
this.localDatabase.closeReplication(); this.localDatabase.closeReplication();
Logger(`Remote database updated to incompatible version. update your Obsidian-livesync plugin.`, LOG_LEVEL.NOTICE); Logger(`Remote database updated to incompatible version. update your self-hosted-livesync plugin.`, LOG_LEVEL.NOTICE);
} }
} }
this.gcHook(); this.gcHook();
} }
} }
async realizeSettingSyncMode() { realizeSettingSyncMode() {
this.localDatabase.closeReplication(); this.localDatabase.closeReplication();
if (this.settings.liveSync) { if (this.settings.liveSync) {
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult); this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
@@ -1547,8 +1662,30 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
refreshStatusText() { refreshStatusText() {
let statusStr = this.localDatabase.status(); let sent = this.localDatabase.docSent;
this.statusBar.setText("Sync:" + statusStr); let arrived = this.localDatabase.docArrived;
let w = "";
switch (this.localDatabase.syncStatus) {
case "CLOSED":
case "COMPLETED":
case "NOT_CONNECTED":
w = "⏹";
break;
case "PAUSED":
w = "💤";
break;
case "CONNECTED":
w = "⚡";
break;
case "ERRORED":
w = "⚠";
break;
default:
w = "?";
}
this.statusBar.title = this.localDatabase.syncStatus;
this.statusBar.setText(`Sync:${w}${sent}${arrived}`);
} }
async replicate(showMessage?: boolean) { async replicate(showMessage?: boolean) {
if (this.settings.versionUpFlash != "") { if (this.settings.versionUpFlash != "") {
@@ -1576,10 +1713,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async syncAllFiles() { async syncAllFiles() {
// synchronize all files between database and storage. // synchronize all files between database and storage.
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.map((e) => e.id); const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:")).map((e) => normalizePath(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);
@@ -1587,19 +1725,25 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const onlyInStorageNames = onlyInStorage.map((e) => e.path); const onlyInStorageNames = onlyInStorage.map((e) => e.path);
const syncFiles = filesStorage.filter((e) => onlyInStorageNames.indexOf(e.path) == -1); 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`);
// just write to DB from storage. // just write to DB from storage.
for (let v of onlyInStorage) { for (let v of onlyInStorage) {
await this.updateIntoDB(v); await this.updateIntoDB(v);
} }
// simply realize it // simply realize it
this.statusBar.setText(`UPDATE STORAGE`);
Logger("Writing files that only in database");
for (let v of onlyInDatabase) { for (let v of onlyInDatabase) {
await this.pullFile(v, filesStorage); await this.pullFile(v, filesStorage);
} }
// have to sync below.. // have to sync below..
this.statusBar.setText(`CHECK FILE STATUS`);
for (let v of syncFiles) { for (let v of syncFiles) {
await this.syncFileBetweenDBandStorage(v, filesStorage); await this.syncFileBetweenDBandStorage(v, filesStorage);
} }
Logger("Initialized");
} }
async deleteFolderOnDB(folder: TFolder) { async deleteFolderOnDB(folder: TFolder) {
Logger(`delete folder:${folder.path}`); Logger(`delete folder:${folder.path}`);
@@ -1611,7 +1755,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger(`->is dir`, LOG_LEVEL.VERBOSE); Logger(`->is dir`, LOG_LEVEL.VERBOSE);
await this.deleteFolderOnDB(entry); await this.deleteFolderOnDB(entry);
try { try {
await this.app.vault.delete(entry); if (this.settings.trashInsteadDelete) {
await this.app.vault.trash(entry, false);
} else {
await this.app.vault.delete(entry);
}
} catch (ex) { } catch (ex) {
if (ex.code && ex.code == "ENOENT") { if (ex.code && ex.code == "ENOENT") {
//NO OP. //NO OP.
@@ -1626,7 +1774,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
try { try {
await this.app.vault.delete(folder); if (this.settings.trashInsteadDelete) {
await this.app.vault.trash(folder, false);
} else {
await this.app.vault.delete(folder);
}
} catch (ex) { } catch (ex) {
if (ex.code && ex.code == "ENOENT") { if (ex.code && ex.code == "ENOENT") {
//NO OP. //NO OP.
@@ -1642,7 +1794,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
let entry = v as TFile & TFolder; let entry = v as TFile & TFolder;
if (entry.children) { if (entry.children) {
await this.deleteFolderOnDB(entry); await this.deleteFolderOnDB(entry);
await this.app.vault.delete(entry); if (this.settings.trashInsteadDelete) {
await this.app.vault.trash(entry, false);
} else {
await this.app.vault.delete(entry);
}
} else { } else {
await this.deleteFromDB(entry); await this.deleteFromDB(entry);
} }
@@ -1770,7 +1926,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 == filename); let targetFiles = fileList.filter((e) => e.path == normalizePath(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);
@@ -1789,7 +1945,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//when to opened file; //when to opened file;
} }
async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) { async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) {
let doc = await this.localDatabase.getDBEntry(file.path); let doc = await this.localDatabase.getDBEntryMeta(file.path);
if (doc === false) return; if (doc === false) return;
if (file.stat.mtime > doc.mtime) { if (file.stat.mtime > doc.mtime) {
//newer local file. //newer local file.
@@ -1798,7 +1954,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} else if (file.stat.mtime < doc.mtime) { } else if (file.stat.mtime < doc.mtime) {
//newer database file. //newer database file.
Logger("sync : older storage files so write from database:" + file.path); Logger("sync : older storage files so write from database:" + file.path);
await this.doc2storate_modify(doc, file); let docx = await this.localDatabase.getDBEntry(file.path);
if (docx != false) {
await this.doc2storate_modify(docx, file);
}
} else { } else {
//eq.case //eq.case
} }
@@ -1995,7 +2154,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
containerEl.empty(); containerEl.empty();
containerEl.createEl("h2", { text: "Settings for obsidian-livesync." }); containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." });
new Setting(containerEl).setName("CouchDB Remote URI").addText((text) => new Setting(containerEl).setName("CouchDB Remote URI").addText((text) =>
text text
@@ -2142,6 +2301,16 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
); );
new Setting(containerEl)
.setName("Trash deleted files")
.setDesc("Do not delete files that deleted in remote, just move to trash.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.trashInsteadDelete).onChange(async (value) => {
this.plugin.settings.trashInsteadDelete = 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.")

View File

@@ -1,9 +1,9 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Obsidian Live sync", "name": "Self-hosted LiveSync",
"version": "0.1.8", "version": "0.1.11",
"minAppVersion": "0.9.12", "minAppVersion": "0.9.12",
"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": "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",
"authorUrl": "https://github.com/vrtmrz", "authorUrl": "https://github.com/vrtmrz",
"isDesktopOnly": false "isDesktopOnly": false

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.1.8", "version": "0.1.11",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.1.8", "version": "0.1.11",
"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.8", "version": "0.1.11",
"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": {

View File

@@ -14,12 +14,17 @@
overflow-y: scroll; overflow-y: scroll;
/* min-height: 280px; */ /* min-height: 280px; */
max-height: 280px; max-height: 280px;
user-select: text;
} }
.op-pre { .op-pre {
white-space: pre-wrap; white-space: pre-wrap;
} }
.op-warn { .op-warn {
border:1px solid salmon; border: 1px solid salmon;
padding:2px; padding: 2px;
border-radius: 4px; border-radius: 4px;
} }
.syncstatusbar {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}