mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-04 00:48:47 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04e3004aca | ||
|
|
53b4d4cd20 | ||
|
|
d324f08240 |
44
README.md
44
README.md
@@ -1,7 +1,10 @@
|
||||
# obsidian-livesync
|
||||
# Self-hosted LiveSync
|
||||
|
||||
This is the obsidian plugin that enables livesync between multi-devices.
|
||||
Runs in Mac, Android, Windows, and iOS.
|
||||
**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.
|
||||
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> -->
|
||||
|
||||
@@ -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!**
|
||||
|
||||
Limitations: Folder deletion handling is not completed.
|
||||
Limitations: ~~Folder deletion handling is not completed.~~ **It would work now.**
|
||||
|
||||
## This plugin enables..
|
||||
|
||||
@@ -26,7 +29,7 @@ If you want to synchronize to both backend, sync one by one, please.
|
||||
## 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)
|
||||
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.
|
||||
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
|
||||
@@ -35,7 +38,7 @@ If you want to synchronize to both backend, sync one by one, please.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
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**.
|
||||
_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. From the most reliable device<sup>(_The device_)</sup>, back your vault up.
|
||||
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.
|
||||
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.
|
||||
@@ -75,7 +78,6 @@ Note: The figure is drawn as single-directional, between two devices. But everyt
|
||||
|
||||

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

|
||||
|
||||
7. In resource details, there's information to connect from obsidian-livesync.
|
||||
Copy the "External Endpoint(preferred)" address. <sup>(\*1)</sup>
|
||||
7. In resource details, there's information to connect from self-hosted-livesync.
|
||||
Copy the "External Endpoint(preferred)" address. <sup>(\*1)</sup>. We use this address later, with the database name.
|
||||

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

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

|
||||
|
||||
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
|
||||
@@ -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.
|
||||
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.
|
||||
open details.
|
||||
@@ -143,16 +147,16 @@ Select Multitenant(it's the default) and the region as you like.
|
||||
follow the figure, it's
|
||||
"apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5"<sup>(\*3)</sup> and "c2c11651d75497fa3d3c486e4c8bdf27"<sup>(\*4)</sup>
|
||||
|
||||
### obsidian-livesync setting
|
||||
### Self-hosted LiveSync setting
|
||||
|
||||

|
||||
example values.
|
||||
|
||||
| Items | Value | example |
|
||||
| ------------------- | ----------- | --------------------------------------------------------------------------- |
|
||||
| CouchDB Remote URI: | (\*1)/(\*2) | https://xxxxxxxxxxxxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud/sync-test |
|
||||
| CouchDB Username | (\*3) | apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5 |
|
||||
| CouchDB Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 |
|
||||
| Items | Value | example |
|
||||
| ------------------- | -------------------------------- | --------------------------------------------------------------------------- |
|
||||
| CouchDB Remote URI: | (\*1)/(\*2) or any favorite name | https://xxxxxxxxxxxxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud/sync-test |
|
||||
| CouchDB Username | (\*3) | apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5 |
|
||||
| CouchDB Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 |
|
||||
|
||||
# License
|
||||
|
||||
|
||||
113
main.ts
113
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 { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||
import xxhash from "xxhash-wasm";
|
||||
@@ -231,7 +231,7 @@ const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
||||
return false;
|
||||
};
|
||||
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, {
|
||||
auth,
|
||||
});
|
||||
@@ -239,6 +239,7 @@ const connectRemoteCouchDB = async (uri: string, auth: { username: string; passw
|
||||
let info = await db.info();
|
||||
return { db: db, info: info };
|
||||
} catch (ex) {
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -662,7 +663,7 @@ class LocalPouchDB {
|
||||
console.log("!" + v.id);
|
||||
} else {
|
||||
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))) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1120,7 +1121,7 @@ class LocalPouchDB {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
let defInitPoint: EntryMilestoneInfo = {
|
||||
@@ -1154,7 +1155,7 @@ class LocalPouchDB {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
let defInitPoint: EntryMilestoneInfo = {
|
||||
@@ -1284,9 +1285,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (delay < 200) delay = 200;
|
||||
if (delay > 5000) delay = 5000;
|
||||
this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false);
|
||||
this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false);
|
||||
this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false);
|
||||
// this.watchVaultDelete = debounce(this.watchVaultDelete.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.watchWindowVisiblity = debounce(this.watchWindowVisiblity.bind(this), delay, false);
|
||||
|
||||
this.registerWatchEvents();
|
||||
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
||||
@@ -1408,16 +1414,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
watchWindowVisiblity() {
|
||||
this.watchWindowVisiblityAsync();
|
||||
}
|
||||
async watchWindowVisiblityAsync() {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
let isHidden = document.hidden;
|
||||
if (isHidden) {
|
||||
this.localDatabase.closeReplication();
|
||||
} else {
|
||||
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) {
|
||||
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
||||
await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
||||
}
|
||||
}
|
||||
this.gcHook();
|
||||
@@ -1425,35 +1434,82 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
watchWorkspaceOpen(file: TFile) {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
this.watchWorkspaceOpenAsync(file);
|
||||
}
|
||||
async watchWorkspaceOpenAsync(file: TFile) {
|
||||
if (file == null) return;
|
||||
this.localDatabase.disposeHashCache();
|
||||
this.showIfConflicted(file);
|
||||
await this.showIfConflicted(file);
|
||||
this.gcHook();
|
||||
}
|
||||
watchVaultChange(file: TFile, ...args: any[]) {
|
||||
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();
|
||||
}
|
||||
watchVaultDelete(file: TFile & TFolder) {
|
||||
watchVaultDelete(file: TFile | TFolder) {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (file.children) {
|
||||
//folder
|
||||
this.deleteFolderOnDB(file);
|
||||
// this.app.vault.delete(file);
|
||||
} else {
|
||||
this.deleteFromDB(file);
|
||||
this.watchVaultDeleteAsync(file);
|
||||
}
|
||||
async watchVaultDeleteAsync(file: TFile | TFolder) {
|
||||
if (file instanceof TFile) {
|
||||
await this.deleteFromDB(file);
|
||||
} else if (file instanceof TFolder) {
|
||||
await this.deleteFolderOnDB(file);
|
||||
}
|
||||
this.gcHook();
|
||||
}
|
||||
watchVaultRename(file: TFile & TFolder, oldFile: any) {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (file.children) {
|
||||
// this.renameFolder(file,oldFile);
|
||||
Logger(`folder name changed:(this operation is not supported) ${file.path}`, LOG_LEVEL.NOTICE);
|
||||
GetAllFilesRecursively(file: TAbstractFile): TFile[] {
|
||||
if (file instanceof TFile) {
|
||||
return [file];
|
||||
} else if (file instanceof TFolder) {
|
||||
let result: TFile[] = [];
|
||||
for (var v of file.children) {
|
||||
result.push(...this.GetAllFilesRecursively(v));
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
this.updateIntoDB(file);
|
||||
this.deleteFromDBbyPath(oldFile);
|
||||
Logger(`Filetype error:${file.path}`, LOG_LEVEL.NOTICE);
|
||||
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();
|
||||
}
|
||||
@@ -1647,7 +1703,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (change.type == "versioninfo") {
|
||||
if (change.version > VER) {
|
||||
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();
|
||||
@@ -1683,6 +1739,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
default:
|
||||
w = "?";
|
||||
}
|
||||
this.statusBar.title = this.localDatabase.syncStatus;
|
||||
this.statusBar.setText(`Sync:${w} ↑${sent} ↓${arrived}`);
|
||||
}
|
||||
async replicate(showMessage?: boolean) {
|
||||
@@ -2152,7 +2209,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
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) =>
|
||||
text
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Obsidian Live sync",
|
||||
"version": "0.1.9",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.1.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",
|
||||
"authorUrl": "https://github.com/vrtmrz",
|
||||
"isDesktopOnly": false
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.1.9",
|
||||
"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.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
overflow-y: scroll;
|
||||
/* min-height: 280px; */
|
||||
max-height: 280px;
|
||||
user-select: text;
|
||||
}
|
||||
.op-pre {
|
||||
white-space: pre-wrap;
|
||||
|
||||
Reference in New Issue
Block a user