mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-13 11:01:16 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
155439ed56 | ||
|
|
04e3004aca | ||
|
|
53b4d4cd20 | ||
|
|
d324f08240 | ||
|
|
0b526e9cea | ||
|
|
07535eb3fc | ||
|
|
9965d123bd | ||
|
|
b1c045937b | ||
|
|
a4fdcf9540 | ||
|
|
a9f06a3ae7 | ||
|
|
0946b1e012 |
70
README.md
70
README.md
@@ -1,15 +1,18 @@
|
|||||||
# 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> -->
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**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.~~ **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.
|
||||||
@@ -43,20 +46,37 @@ Note: Please read "Limitations" carefully. Do not send your private vault.
|
|||||||
Available from on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
Available from on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||||
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
|
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
|
||||||
|
|
||||||
## When your database looks corrupted
|
## 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._
|
||||||
|
|
||||||
|
1. Update plugin on all devices.
|
||||||
1. Disable any synchronizations on all devices.
|
1. Disable any synchronizations on all devices.
|
||||||
2. 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.
|
||||||
3. Click "Reset local database" on all devices.
|
1. Press "Drop History"-> "Execute" button from _The device_.
|
||||||
4. From _The device_ click "Reset remote database".
|
1. Wait for a while, so self-hosted-livesync will say "completed."
|
||||||
5. From _The device_ click "Init Database again".
|
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.
|
||||||
6. Enable any sync or Hit the Replication button.
|
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.
|
||||||
|
|
||||||
And wait for a minute. your data will be uploaded and synchronized with all devices again.
|
# Designed architecture
|
||||||
|
|
||||||
|
## How does this plugin synchronize.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. When notes are created or modified, Obsidian raises some events. obsidian-live-sync catch these events and reflect changes into Local PouchDB.
|
||||||
|
2. PouchDB automatically or manually replicates changes to remote CouchDB.
|
||||||
|
3. Another device is watching remote CouchDB's changes, so retrieve new changes.
|
||||||
|
4. obsidian-live-sync reflects replicated changeset into Obsidian's vault.
|
||||||
|
|
||||||
|
Note: The figure is drawn as single-directional, between two devices. But everything occurs bi-directionally between many devices at once in real.
|
||||||
|
|
||||||
|
## Techniques to keep bandwidth low.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Cloudant Setup
|
## Cloudant Setup
|
||||||
|
|
||||||
@@ -84,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.
|
||||||

|

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

|

|
||||||
|
|
||||||
### CouchDB setup
|
### CouchDB setup
|
||||||
@@ -104,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.
|
||||||

|

|
||||||
|
|
||||||
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
|
||||||
@@ -115,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 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.
|
||||||
@@ -125,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
|
||||||
|
|
||||||

|

|
||||||
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
|
||||||
|
|
||||||
|
|||||||
BIN
images/1.png
Normal file
BIN
images/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
images/2.png
Normal file
BIN
images/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 223 KiB |
346
main.ts
346
main.ts
@@ -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, 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";
|
||||||
@@ -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;
|
||||||
@@ -619,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,15 +1277,23 @@ 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;
|
||||||
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);
|
||||||
@@ -1278,6 +1376,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() {
|
||||||
@@ -1314,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();
|
||||||
@@ -1331,43 +1435,92 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
//--> 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 +1534,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 +1574,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 +1588,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 +1602,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,9 +1643,13 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (doc.datatype == "plain") {
|
if (doc.datatype == "plain") {
|
||||||
@@ -1489,9 +1658,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -1523,6 +1696,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;
|
||||||
@@ -1534,13 +1708,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);
|
||||||
@@ -1548,8 +1722,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 != "") {
|
||||||
@@ -1577,10 +1773,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);
|
||||||
@@ -1588,19 +1785,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}`);
|
||||||
@@ -1612,7 +1815,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.
|
||||||
@@ -1627,7 +1834,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.
|
||||||
@@ -1643,7 +1854,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);
|
||||||
}
|
}
|
||||||
@@ -1771,7 +1986,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);
|
||||||
@@ -1790,7 +2005,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.
|
||||||
@@ -1799,7 +2014,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
|
||||||
}
|
}
|
||||||
@@ -1996,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
|
||||||
@@ -2143,6 +2361,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.")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Obsidian Live sync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.1.7",
|
"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
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.1.7",
|
"version": "0.1.13",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.1.7",
|
"version": "0.1.13",
|
||||||
"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.7",
|
"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": {
|
||||||
|
|||||||
@@ -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%);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user