Compare commits

...

4 Commits

Author SHA1 Message Date
vorotamoroz
155439ed56 URG, attachment doesn't captured. 2021-11-10 10:06:36 +09:00
vorotamoroz
04e3004aca Improved:
Folder deletion and renaming are now tracked.
Database update fixed up. But be a little heavier.
Touched up the readme.
2021-11-09 15:05:12 +09:00
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
6 changed files with 122 additions and 55 deletions

View File

@@ -1,7 +1,10 @@
# obsidian-livesync # Self-hosted LiveSync
This is the obsidian plugin that enables livesync between multi-devices. **Renamed from: obsidian-livesync**
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> -->
@@ -9,7 +12,7 @@ Runs in Mac, Android, Windows, and iOS.
**It's getting almost stable now, But 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.~~ **It would work now.**
## This plugin enables.. ## This plugin enables..
@@ -26,7 +29,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 +38,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 +48,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 +56,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.
@@ -75,7 +78,6 @@ Note: The figure is drawn as single-directional, between two devices. But everyt
![dedupe](images/2.png) ![dedupe](images/2.png)
## Cloudant Setup ## Cloudant Setup
### Creating an Instance ### Creating an Instance
@@ -102,8 +104,8 @@ 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>. We use this address later, with the database name.
![step 9](instruction_images/cloudant_9.png) ![step 9](instruction_images/cloudant_9.png)
### CouchDB setup ### CouchDB setup
@@ -122,7 +124,9 @@ Select Multitenant(it's the default) and the region as you like.
Enter the name as you like <sup>(\*2)</sup> and Hit the "Create" button below. Enter the name as you like <sup>(\*2)</sup> and Hit the "Create" button below.
![step 3](instruction_images/couchdb_3.png) ![step 3](instruction_images/couchdb_3.png)
1. If the database was shown with joyful messages, then you can close this browser tab now. 1. If the database was shown with joyful messages, setup is almost done.
And, once you have confirmed that you can create a database, usullay there is no need to open this screen.
You can create a database from Self-hosted LiveSync.
![step 4](instruction_images/couchdb_4.png) ![step 4](instruction_images/couchdb_4.png)
### Credentials Setup ### Credentials Setup
@@ -133,7 +137,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,16 +147,16 @@ 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.
| Items | Value | example | | Items | Value | example |
| ------------------- | ----------- | --------------------------------------------------------------------------- | | ------------------- | -------------------------------- | --------------------------------------------------------------------------- |
| CouchDB Remote URI: | (\*1)/(\*2) | https://xxxxxxxxxxxxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud/sync-test | | CouchDB Remote URI: | (\*1)/(\*2) or any favorite name | https://xxxxxxxxxxxxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud/sync-test |
| CouchDB Username | (\*3) | apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5 | | CouchDB Username | (\*3) | apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5 |
| CouchDB Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 | | CouchDB Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 |
# License # License

120
main.ts
View File

@@ -1,4 +1,4 @@
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath } from "obsidian"; import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath, TAbstractFile } 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";
@@ -231,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,
}); });
@@ -239,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;
} }
}; };
@@ -662,7 +663,7 @@ class LocalPouchDB {
console.log("!" + v.id); console.log("!" + v.id);
} else { } else {
if (!v.id.startsWith("h:")) { if (!v.id.startsWith("h:")) {
console.log("?" + v.id); // console.log("?" + v.id);
} }
} }
} }
@@ -939,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;
} }
@@ -1120,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 = {
@@ -1154,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 = {
@@ -1283,10 +1284,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
let delay = this.settings.savingDelay; let delay = this.settings.savingDelay;
if (delay < 200) delay = 200; if (delay < 200) delay = 200;
if (delay > 5000) delay = 5000; if (delay > 5000) delay = 5000;
this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false); // this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false);
this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false); // this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false);
this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false); // this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false);
this.watchVaultChange = this.watchVaultChange.bind(this);
this.watchVaultCreate = this.watchVaultCreate.bind(this);
this.watchVaultDelete = this.watchVaultDelete.bind(this);
this.watchVaultRename = this.watchVaultRename.bind(this);
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), delay, false); this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), delay, false);
this.watchWindowVisiblity = debounce(this.watchWindowVisiblity.bind(this), delay, false);
this.registerWatchEvents(); this.registerWatchEvents();
this.parseReplicationResult = this.parseReplicationResult.bind(this); this.parseReplicationResult = this.parseReplicationResult.bind(this);
@@ -1408,16 +1415,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
watchWindowVisiblity() { watchWindowVisiblity() {
this.watchWindowVisiblityAsync();
}
async watchWindowVisiblityAsync() {
if (this.settings.suspendFileWatching) return; if (this.settings.suspendFileWatching) return;
let isHidden = document.hidden; let isHidden = document.hidden;
if (isHidden) { if (isHidden) {
this.localDatabase.closeReplication(); this.localDatabase.closeReplication();
} else { } else {
if (this.settings.liveSync) { if (this.settings.liveSync) {
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult); await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
} }
if (this.settings.syncOnStart) { if (this.settings.syncOnStart) {
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult); await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
} }
} }
this.gcHook(); this.gcHook();
@@ -1425,35 +1435,86 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
watchWorkspaceOpen(file: TFile) { watchWorkspaceOpen(file: TFile) {
if (this.settings.suspendFileWatching) return; if (this.settings.suspendFileWatching) return;
this.watchWorkspaceOpenAsync(file);
}
async watchWorkspaceOpenAsync(file: TFile) {
if (file == null) return; if (file == null) return;
this.localDatabase.disposeHashCache(); this.localDatabase.disposeHashCache();
this.showIfConflicted(file); await this.showIfConflicted(file);
this.gcHook(); this.gcHook();
} }
watchVaultCreate(file: TFile, ...args: any[]) {
if (this.settings.suspendFileWatching) return;
this.watchVaultChangeAsync(file, ...args);
}
watchVaultChange(file: TFile, ...args: any[]) { watchVaultChange(file: TFile, ...args: any[]) {
if (this.settings.suspendFileWatching) return; if (this.settings.suspendFileWatching) return;
this.updateIntoDB(file); this.watchVaultChangeAsync(file, ...args);
}
batchFileChange: string[] = [];
async watchVaultChangeAsync(file: TFile, ...args: any[]) {
await this.updateIntoDB(file);
this.gcHook(); this.gcHook();
} }
watchVaultDelete(file: TFile & TFolder) { watchVaultDelete(file: TFile | TFolder) {
if (this.settings.suspendFileWatching) return; if (this.settings.suspendFileWatching) return;
if (file.children) { this.watchVaultDeleteAsync(file);
//folder }
this.deleteFolderOnDB(file); async watchVaultDeleteAsync(file: TFile | TFolder) {
// this.app.vault.delete(file); if (file instanceof TFile) {
} else { await this.deleteFromDB(file);
this.deleteFromDB(file); } else if (file instanceof TFolder) {
await this.deleteFolderOnDB(file);
} }
this.gcHook(); this.gcHook();
} }
watchVaultRename(file: TFile & TFolder, oldFile: any) { GetAllFilesRecursively(file: TAbstractFile): TFile[] {
if (this.settings.suspendFileWatching) return; if (file instanceof TFile) {
if (file.children) { return [file];
// this.renameFolder(file,oldFile); } else if (file instanceof TFolder) {
Logger(`folder name changed:(this operation is not supported) ${file.path}`, LOG_LEVEL.NOTICE); let result: TFile[] = [];
for (var v of file.children) {
result.push(...this.GetAllFilesRecursively(v));
}
return result;
} else { } else {
this.updateIntoDB(file); Logger(`Filetype error:${file.path}`, LOG_LEVEL.NOTICE);
this.deleteFromDBbyPath(oldFile); throw new Error(`Filetype error:${file.path}`);
}
}
watchVaultRename(file: TFile | TFolder, oldFile: any) {
if (this.settings.suspendFileWatching) return;
this.watchVaultRenameAsync(file, oldFile);
}
getFilePath(file: TAbstractFile): string {
if (file instanceof TFolder) {
if (file.isRoot()) return "";
return this.getFilePath(file.parent) + "/" + file.name;
}
if (file instanceof TFile) {
return this.getFilePath(file.parent) + "/" + file.name;
}
}
async watchVaultRenameAsync(file: TFile | TFolder, oldFile: any) {
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
if (file instanceof TFolder) {
const newFiles = this.GetAllFilesRecursively(file);
// for guard edge cases. this won't happen and each file's event will be raise.
for (const i of newFiles) {
let newFilePath = normalizePath(this.getFilePath(i));
let newFile = this.app.vault.getAbstractFileByPath(newFilePath);
if (newFile instanceof TFile) {
Logger(`save ${newFile.path} into db`);
await this.updateIntoDB(newFile);
}
}
Logger(`delete below ${oldFile} from db`);
await this.deleteFromDBbyPath(oldFile);
} else if (file instanceof TFile) {
Logger(`file save ${file.path} into db`);
await this.updateIntoDB(file);
Logger(`deleted ${oldFile} into db`);
await this.deleteFromDBbyPath(oldFile);
} }
this.gcHook(); this.gcHook();
} }
@@ -1647,7 +1708,7 @@ 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();
@@ -1683,6 +1744,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
default: default:
w = "?"; w = "?";
} }
this.statusBar.title = this.localDatabase.syncStatus;
this.statusBar.setText(`Sync:${w}${sent}${arrived}`); this.statusBar.setText(`Sync:${w}${sent}${arrived}`);
} }
async replicate(showMessage?: boolean) { async replicate(showMessage?: boolean) {
@@ -2152,7 +2214,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

View File

@@ -1,9 +1,9 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Obsidian Live sync", "name": "Self-hosted LiveSync",
"version": "0.1.9", "version": "0.1.13",
"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.9", "version": "0.1.13",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.1.9", "version": "0.1.13",
"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.9", "version": "0.1.13",
"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,6 +14,7 @@
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;