mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-22 06:01:28 +00:00
Improved:
Folder deletion and renaming are now tracked. Database update fixed up. But be a little heavier. Touched up the readme.
This commit is contained in:
26
README.md
26
README.md
@@ -1,4 +1,5 @@
|
|||||||
# Self-hosted LiveSync
|
# Self-hosted LiveSync
|
||||||
|
|
||||||
**Renamed from: obsidian-livesync**
|
**Renamed from: obsidian-livesync**
|
||||||
|
|
||||||
This is the obsidian plugin that enables livesync between multi-devices with self-hosted database.
|
This is the obsidian plugin that enables livesync between multi-devices with self-hosted database.
|
||||||
@@ -11,7 +12,7 @@ Community implementation, not compatible with official "Sync".
|
|||||||
|
|
||||||
**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..
|
||||||
|
|
||||||
@@ -77,7 +78,6 @@ Note: The figure is drawn as single-directional, between two devices. But everyt
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Cloudant Setup
|
## Cloudant Setup
|
||||||
|
|
||||||
### Creating an Instance
|
### Creating an Instance
|
||||||
@@ -105,7 +105,7 @@ Select Multitenant(it's the default) and the region as you like.
|
|||||||

|

|
||||||
|
|
||||||
7. In resource details, there's information to connect from self-hosted-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.
|
||||||

|

|
||||||
|
|
||||||
### CouchDB setup
|
### CouchDB setup
|
||||||
@@ -120,11 +120,13 @@ Select Multitenant(it's the default) and the region as you like.
|
|||||||
_NOTE: of course We want to set "app://obsidian.md" but it's not acceptable on Cloudant._
|
_NOTE: of course We want to set "app://obsidian.md" but it's not acceptable on Cloudant._
|
||||||

|

|
||||||
|
|
||||||
1. And open the "Databases" tab and hit the "Create Database" button.
|
1. And open the "Databases" tab and hit the "Create Database" button.
|
||||||
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.
|
||||||

|

|
||||||
|
|
||||||
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.
|
||||||

|

|
||||||
|
|
||||||
### Credentials Setup
|
### Credentials Setup
|
||||||
@@ -135,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.
|
||||||

|

|
||||||
_NOTE: This "name" is not related to your username that uses in self-hosted-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.
|
||||||
@@ -145,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>
|
||||||
|
|
||||||
### self-hosted-livesync setting
|
### Self-hosted LiveSync setting
|
||||||
|
|
||||||

|

|
||||||
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
|
||||||
|
|
||||||
|
|||||||
99
main.ts
99
main.ts
@@ -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";
|
||||||
@@ -663,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1285,9 +1285,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
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.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);
|
||||||
@@ -1409,16 +1414,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();
|
||||||
@@ -1426,35 +1434,82 @@ 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();
|
||||||
}
|
}
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"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
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.1.11",
|
"version": "0.1.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": "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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user