mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-03 06:11:50 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fbecdf567 | ||
|
|
5db3a374a9 | ||
|
|
6f76f90075 | ||
|
|
9acf9fe093 | ||
|
|
1e3de47d92 | ||
|
|
a50f0965f6 | ||
|
|
9d3aa35b0b | ||
|
|
b4b9684a55 | ||
|
|
221cccb845 | ||
|
|
801500f924 | ||
|
|
3545ae9690 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "src/lib"]
|
||||||
|
path = src/lib
|
||||||
|
url = https://github.com/vrtmrz/livesync-commonlib
|
||||||
@@ -18,7 +18,14 @@ Note: This password is saved into your Obsidian's vault in plain text.
|
|||||||
The Database name to synchronize.
|
The Database name to synchronize.
|
||||||
⚠️If not exist, created automatically.
|
⚠️If not exist, created automatically.
|
||||||
|
|
||||||
|
|
||||||
|
### Use the old connecting method
|
||||||
|
Since v0.8.0, Self-hosted LiveSync uses Obsidian's API to connect to the CouchDB instead of the browser API.
|
||||||
|
This method will increase the performance and avoid troubles with the CORS.
|
||||||
|
But it doesn't been well tested yet. If you are troubled, please disable this option once.
|
||||||
|
|
||||||
### Test Database connection
|
### Test Database connection
|
||||||
|
You can check the connection by clicking this button.
|
||||||
|
|
||||||
## Local Database Configurations
|
## Local Database Configurations
|
||||||
"Local Database" is created inside your obsidian.
|
"Local Database" is created inside your obsidian.
|
||||||
@@ -44,6 +51,8 @@ As a result, Obsidian's behavior is temporarily slowed down.
|
|||||||
Default is 300 seconds.
|
Default is 300 seconds.
|
||||||
If you are an early adopter, maybe this value is left as 30 seconds. Please change this value to larger values.
|
If you are an early adopter, maybe this value is left as 30 seconds. Please change this value to larger values.
|
||||||
|
|
||||||
|
Note: If you want to use "Use history", this vault must be set to 0.
|
||||||
|
|
||||||
### Manual Garbage Collect
|
### Manual Garbage Collect
|
||||||
Run "Garbage Collection" manually.
|
Run "Garbage Collection" manually.
|
||||||
|
|
||||||
@@ -52,6 +61,8 @@ Encrypt your database. It affects only the database, your files are left as plai
|
|||||||
|
|
||||||
The encryption algorithm is AES-GCM.
|
The encryption algorithm is AES-GCM.
|
||||||
|
|
||||||
|
Note: If you want to use "Plugins and their settings", you have to enable this.
|
||||||
|
|
||||||
### Passphrase
|
### Passphrase
|
||||||
The passphrase to used as the key of encryption. Please use the long text.
|
The passphrase to used as the key of encryption. Please use the long text.
|
||||||
|
|
||||||
@@ -195,6 +206,29 @@ You can set synchronization method at once as these pattern:
|
|||||||
- Sync on File Open : disabled
|
- Sync on File Open : disabled
|
||||||
- Sync on Start : disabled
|
- Sync on Start : disabled
|
||||||
|
|
||||||
|
### Use history
|
||||||
|
If you enable this option, you can keep document histories in your database.
|
||||||
|
(Not all intermediate changes are synchronized.)
|
||||||
|
You can check the changes caused by your edit and/or replication.
|
||||||
|
|
||||||
|
### Enable plugin synchronization
|
||||||
|
If you want to use this feature, you have to activate this feature by this switch.
|
||||||
|
|
||||||
|
### Sweep plugins automatically
|
||||||
|
Plugin sweep will run before replication automatically.
|
||||||
|
|
||||||
|
### Sweep plugins periodically
|
||||||
|
Plugin sweep will run each 1 minute.
|
||||||
|
|
||||||
|
### Notify updates
|
||||||
|
When replication is complete, a message will be notified if a newer version of the plugin applied to this device is configured on another device.
|
||||||
|
|
||||||
|
### Device and Vault name
|
||||||
|
To save the plugins, you have to set a unique name every each device.
|
||||||
|
|
||||||
|
### Open
|
||||||
|
Open the "Plugins and their settings" dialog.
|
||||||
|
|
||||||
## Hatch
|
## Hatch
|
||||||
From here, everything is under the hood. Please handle it with care.
|
From here, everything is under the hood. Please handle it with care.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.7.2",
|
"version": "0.8.7",
|
||||||
"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",
|
||||||
|
|||||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -1,15 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.7.2",
|
"version": "0.8.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.7.2",
|
"version": "0.8.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"esbuild": "0.13.12",
|
||||||
|
"esbuild-svelte": "^0.6.0",
|
||||||
|
"svelte-preprocess": "^4.10.2",
|
||||||
"xxhash-wasm": "^0.4.2"
|
"xxhash-wasm": "^0.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -27,7 +30,7 @@
|
|||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-airbnb-base": "^14.2.1",
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
"eslint-plugin-import": "^2.25.2",
|
"eslint-plugin-import": "^2.25.2",
|
||||||
"obsidian": "^0.13.11",
|
"obsidian": "^0.14.6",
|
||||||
"rollup": "^2.32.1",
|
"rollup": "^2.32.1",
|
||||||
"svelte-preprocess": "^4.10.2",
|
"svelte-preprocess": "^4.10.2",
|
||||||
"tslib": "^2.2.0",
|
"tslib": "^2.2.0",
|
||||||
@@ -2553,9 +2556,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
@@ -2571,9 +2574,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.29.1",
|
"version": "2.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
@@ -2659,15 +2662,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/obsidian": {
|
"node_modules/obsidian": {
|
||||||
"version": "0.13.11",
|
"version": "0.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.14.6.tgz",
|
||||||
"integrity": "sha512-KxOvAh4CG5vzcukmHvyuK9hUIr6ZFlM9FQfGZEwrrEV8VG2/W2Tk5cWrg0VM7EkGE3QBmjX6owjIDIO8QDXVUQ==",
|
"integrity": "sha512-oXPJ8Zt10WhN19bk5l4mZuXRZbbdT1QoMgxGGJ0bB7UcJa0bozDzugS5L/QiV9gDoujpUPxDWNVahEel6r0Fpw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^0.19.6",
|
"@codemirror/state": "^0.19.6",
|
||||||
"@codemirror/view": "^0.19.31",
|
"@codemirror/view": "^0.19.31",
|
||||||
"@types/codemirror": "0.0.108",
|
"@types/codemirror": "0.0.108",
|
||||||
"moment": "2.29.1"
|
"moment": "2.29.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
@@ -5391,9 +5394,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
@@ -5406,9 +5409,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.29.1",
|
"version": "2.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
@@ -5470,15 +5473,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"obsidian": {
|
"obsidian": {
|
||||||
"version": "0.13.11",
|
"version": "0.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.14.6.tgz",
|
||||||
"integrity": "sha512-KxOvAh4CG5vzcukmHvyuK9hUIr6ZFlM9FQfGZEwrrEV8VG2/W2Tk5cWrg0VM7EkGE3QBmjX6owjIDIO8QDXVUQ==",
|
"integrity": "sha512-oXPJ8Zt10WhN19bk5l4mZuXRZbbdT1QoMgxGGJ0bB7UcJa0bozDzugS5L/QiV9gDoujpUPxDWNVahEel6r0Fpw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@codemirror/state": "^0.19.6",
|
"@codemirror/state": "^0.19.6",
|
||||||
"@codemirror/view": "^0.19.31",
|
"@codemirror/view": "^0.19.31",
|
||||||
"@types/codemirror": "0.0.108",
|
"@types/codemirror": "0.0.108",
|
||||||
"moment": "2.29.1"
|
"moment": "2.29.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"once": {
|
"once": {
|
||||||
|
|||||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.7.2",
|
"version": "0.8.7",
|
||||||
"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",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -21,20 +21,23 @@
|
|||||||
"@types/pouchdb-browser": "^6.1.3",
|
"@types/pouchdb-browser": "^6.1.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"eslint": "^7.32.0",
|
|
||||||
"eslint-config-airbnb-base": "^14.2.1",
|
|
||||||
"eslint-plugin-import": "^2.25.2",
|
|
||||||
"obsidian": "^0.13.11",
|
|
||||||
"rollup": "^2.32.1",
|
|
||||||
"tslib": "^2.2.0",
|
|
||||||
"typescript": "^4.2.4",
|
|
||||||
"builtin-modules": "^3.2.0",
|
"builtin-modules": "^3.2.0",
|
||||||
"esbuild": "0.13.12",
|
"esbuild": "0.13.12",
|
||||||
"esbuild-svelte": "^0.6.0",
|
"esbuild-svelte": "^0.6.0",
|
||||||
"svelte-preprocess": "^4.10.2"
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
|
"eslint-plugin-import": "^2.25.2",
|
||||||
|
"obsidian": "^0.14.6",
|
||||||
|
"rollup": "^2.32.1",
|
||||||
|
"svelte-preprocess": "^4.10.2",
|
||||||
|
"tslib": "^2.2.0",
|
||||||
|
"typescript": "^4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"esbuild": "0.13.12",
|
||||||
|
"esbuild-svelte": "^0.6.0",
|
||||||
|
"svelte-preprocess": "^4.10.2",
|
||||||
"xxhash-wasm": "^0.4.2"
|
"xxhash-wasm": "^0.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
# PouchDB-browser
|
# PouchDB-browser
|
||||||
just webpacked.
|
|
||||||
|
Just webpacked.
|
||||||
|
(Rollup couldn't pack pouchdb-browser into browser bundle)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
9820
pouchdb-browser-webpack/package-lock.json
generated
9820
pouchdb-browser-webpack/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pouchdb-browser": "^7.2.2"
|
"pouchdb-browser": "^7.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"webpack": "^5.58.1",
|
"webpack": "^5.58.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { App, Modal } from "obsidian";
|
import { App, Modal } from "obsidian";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||||
import { diff_result } from "./types";
|
import { diff_result } from "./lib/src/types";
|
||||||
import { escapeStringToHTML } from "./utils";
|
import { escapeStringToHTML } from "./lib/src/utils";
|
||||||
|
|
||||||
export class ConflictResolveModal extends Modal {
|
export class ConflictResolveModal extends Modal {
|
||||||
// result: Array<[number, string]>;
|
// result: Array<[number, string]>;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { TFile, Modal, App } from "obsidian";
|
import { TFile, Modal, App } from "obsidian";
|
||||||
import { path2id, escapeStringToHTML } from "./utils";
|
import { path2id } from "./utils";
|
||||||
|
import { escapeStringToHTML } from "./lib/src/utils";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
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 { LOG_LEVEL } from "./lib/src/types";
|
||||||
|
import { Logger } from "./lib/src/logger";
|
||||||
|
|
||||||
export class DocumentHistoryModal extends Modal {
|
export class DocumentHistoryModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
@@ -14,6 +17,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
file: string;
|
file: string;
|
||||||
|
|
||||||
revs_info: PouchDB.Core.RevisionInfo[] = [];
|
revs_info: PouchDB.Core.RevisionInfo[] = [];
|
||||||
|
currentText = "";
|
||||||
|
|
||||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile) {
|
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile) {
|
||||||
super(app);
|
super(app);
|
||||||
@@ -37,6 +41,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
||||||
const rev = this.revs_info[index];
|
const rev = this.revs_info[index];
|
||||||
const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false);
|
const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false);
|
||||||
|
this.currentText = "";
|
||||||
|
|
||||||
if (w === false) {
|
if (w === false) {
|
||||||
this.info.innerHTML = "";
|
this.info.innerHTML = "";
|
||||||
@@ -44,6 +49,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
} else {
|
} else {
|
||||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||||
let result = "";
|
let result = "";
|
||||||
|
this.currentText = w.data;
|
||||||
if (this.showDiff) {
|
if (this.showDiff) {
|
||||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||||
@@ -124,6 +130,14 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
this.contentView = div;
|
this.contentView = div;
|
||||||
div.addClass("op-scrollable");
|
div.addClass("op-scrollable");
|
||||||
div.addClass("op-pre");
|
div.addClass("op-pre");
|
||||||
|
const buttons = contentEl.createDiv("");
|
||||||
|
buttons.createEl("button", { text: "Copy to clipboard" }, (e) => {
|
||||||
|
e.addClass("mod-cta");
|
||||||
|
e.addEventListener("click", async () => {
|
||||||
|
await navigator.clipboard.writeText(this.currentText);
|
||||||
|
Logger(`Old content copied to clipboard`, LOG_LEVEL.NOTICE);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
onClose() {
|
onClose() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Notice } from "obsidian";
|
import { PouchDB } from "./pouchdb-browser";
|
||||||
import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
|
||||||
import xxhash from "xxhash-wasm";
|
import xxhash from "xxhash-wasm";
|
||||||
import {
|
import {
|
||||||
Entry,
|
Entry,
|
||||||
@@ -10,7 +9,6 @@ import {
|
|||||||
NewEntry,
|
NewEntry,
|
||||||
PlainEntry,
|
PlainEntry,
|
||||||
LoadedEntry,
|
LoadedEntry,
|
||||||
ObsidianLiveSyncSettings,
|
|
||||||
Credential,
|
Credential,
|
||||||
EntryMilestoneInfo,
|
EntryMilestoneInfo,
|
||||||
LOG_LEVEL,
|
LOG_LEVEL,
|
||||||
@@ -22,16 +20,20 @@ import {
|
|||||||
VER,
|
VER,
|
||||||
MILSTONE_DOCID,
|
MILSTONE_DOCID,
|
||||||
DatabaseConnectingStatus,
|
DatabaseConnectingStatus,
|
||||||
} from "./types";
|
} from "./lib/src/types";
|
||||||
import { resolveWithIgnoreKnownError, delay, path2id, runWithLock, isPlainText } from "./utils";
|
import { decrypt, encrypt } from "./lib/src/e2ee";
|
||||||
import { Logger } from "./logger";
|
import { RemoteDBSettings } from "./lib/src/types";
|
||||||
|
import { resolveWithIgnoreKnownError, delay, runWithLock, isPlainText, splitPieces, NewNotice, WrappedNotice } from "./lib/src/utils";
|
||||||
|
import { path2id } from "./utils";
|
||||||
|
import { Logger } from "./lib/src/logger";
|
||||||
import { checkRemoteVersion, connectRemoteCouchDB, getLastPostFailedBySize } from "./utils_couchdb";
|
import { checkRemoteVersion, connectRemoteCouchDB, getLastPostFailedBySize } from "./utils_couchdb";
|
||||||
import { decrypt, encrypt } from "./e2ee";
|
|
||||||
|
type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>;
|
||||||
|
|
||||||
export class LocalPouchDB {
|
export class LocalPouchDB {
|
||||||
auth: Credential;
|
auth: Credential;
|
||||||
dbname: string;
|
dbname: string;
|
||||||
settings: ObsidianLiveSyncSettings;
|
settings: RemoteDBSettings;
|
||||||
localDatabase: PouchDB.Database<EntryDoc>;
|
localDatabase: PouchDB.Database<EntryDoc>;
|
||||||
nodeid = "";
|
nodeid = "";
|
||||||
isReady = false;
|
isReady = false;
|
||||||
@@ -52,7 +54,7 @@ export class LocalPouchDB {
|
|||||||
remoteLockedAndDeviceNotAccepted = false;
|
remoteLockedAndDeviceNotAccepted = false;
|
||||||
|
|
||||||
changeHandler: PouchDB.Core.Changes<EntryDoc> = null;
|
changeHandler: PouchDB.Core.Changes<EntryDoc> = null;
|
||||||
syncHandler: PouchDB.Replication.Sync<EntryDoc> = null;
|
syncHandler: PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc> = null;
|
||||||
|
|
||||||
leafArrivedCallbacks: { [key: string]: (() => void)[] } = {};
|
leafArrivedCallbacks: { [key: string]: (() => void)[] } = {};
|
||||||
|
|
||||||
@@ -61,6 +63,8 @@ export class LocalPouchDB {
|
|||||||
docSent = 0;
|
docSent = 0;
|
||||||
docSeq = "";
|
docSeq = "";
|
||||||
|
|
||||||
|
isMobile = false;
|
||||||
|
|
||||||
cancelHandler<T extends PouchDB.Core.Changes<EntryDoc> | PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>>(handler: T): T {
|
cancelHandler<T extends PouchDB.Core.Changes<EntryDoc> | PouchDB.Replication.Sync<EntryDoc> | PouchDB.Replication.Replication<EntryDoc>>(handler: T): T {
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
handler.removeAllListeners();
|
handler.removeAllListeners();
|
||||||
@@ -77,7 +81,7 @@ export class LocalPouchDB {
|
|||||||
this.localDatabase.removeAllListeners();
|
this.localDatabase.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(settings: ObsidianLiveSyncSettings, dbname: string) {
|
constructor(settings: RemoteDBSettings, dbname: string, isMobile: boolean) {
|
||||||
this.auth = {
|
this.auth = {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -85,6 +89,7 @@ export class LocalPouchDB {
|
|||||||
this.dbname = dbname;
|
this.dbname = dbname;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.cancelHandler = this.cancelHandler.bind(this);
|
this.cancelHandler = this.cancelHandler.bind(this);
|
||||||
|
this.isMobile = isMobile;
|
||||||
|
|
||||||
// this.initializeDatabase();
|
// this.initializeDatabase();
|
||||||
}
|
}
|
||||||
@@ -503,7 +508,7 @@ export class LocalPouchDB {
|
|||||||
}
|
}
|
||||||
async putDBEntry(note: LoadedEntry) {
|
async putDBEntry(note: LoadedEntry) {
|
||||||
await this.waitForGCComplete();
|
await this.waitForGCComplete();
|
||||||
let leftData = note.data;
|
// let leftData = note.data;
|
||||||
const savenNotes = [];
|
const savenNotes = [];
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
let made = 0;
|
let made = 0;
|
||||||
@@ -516,53 +521,22 @@ export class LocalPouchDB {
|
|||||||
pieceSize = MAX_DOC_SIZE;
|
pieceSize = MAX_DOC_SIZE;
|
||||||
plainSplit = true;
|
plainSplit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newLeafs: EntryLeaf[] = [];
|
const newLeafs: EntryLeaf[] = [];
|
||||||
do {
|
// To keep low bandwith and database size,
|
||||||
// To keep low bandwith and database size,
|
// Dedup pieces on database.
|
||||||
// Dedup pieces on database.
|
// from 0.1.10, for best performance. we use markdown delimiters
|
||||||
// from 0.1.10, for best performance. we use markdown delimiters
|
// 1. \n[^\n]{longLineThreshold}[^\n]*\n -> long sentence shuld break.
|
||||||
// 1. \n[^\n]{longLineThreshold}[^\n]*\n -> long sentence shuld break.
|
// 2. \n\n shold break
|
||||||
// 2. \n\n shold break
|
// 3. \r\n\r\n should break
|
||||||
// 3. \r\n\r\n should break
|
// 4. \n# should break.
|
||||||
// 4. \n# should break.
|
let minimumChunkSize = this.settings.minimumChunkSize;
|
||||||
let cPieceSize = pieceSize;
|
if (minimumChunkSize < 10) minimumChunkSize = 10;
|
||||||
if (plainSplit) {
|
let longLineThreshold = this.settings.longLineThreshold;
|
||||||
let minimumChunkSize = this.settings.minimumChunkSize;
|
if (longLineThreshold < 100) longLineThreshold = 100;
|
||||||
if (minimumChunkSize < 10) minimumChunkSize = 10;
|
|
||||||
let longLineThreshold = this.settings.longLineThreshold;
|
|
||||||
if (longLineThreshold < 100) longLineThreshold = 100;
|
|
||||||
cPieceSize = 0;
|
|
||||||
// lookup for next splittion .
|
|
||||||
// we're standing on "\n"
|
|
||||||
do {
|
|
||||||
const n1 = leftData.indexOf("\n", cPieceSize + 1);
|
|
||||||
const n2 = leftData.indexOf("\n\n", cPieceSize + 1);
|
|
||||||
const n3 = leftData.indexOf("\r\n\r\n", cPieceSize + 1);
|
|
||||||
const n4 = leftData.indexOf("\n#", cPieceSize + 1);
|
|
||||||
if (n1 == -1 && n2 == -1 && n3 == -1 && n4 == -1) {
|
|
||||||
cPieceSize = MAX_DOC_SIZE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n1 > longLineThreshold) {
|
const pieces = splitPieces(note.data, pieceSize, plainSplit, minimumChunkSize, longLineThreshold);
|
||||||
// long sentence is an established piece
|
for (const piece of pieces()) {
|
||||||
cPieceSize = n1;
|
|
||||||
} else {
|
|
||||||
// cPieceSize = Math.min.apply([n2, n3, n4].filter((e) => e > 1));
|
|
||||||
// ^ heavy.
|
|
||||||
if (n1 > 0 && cPieceSize < n1) cPieceSize = n1;
|
|
||||||
if (n2 > 0 && cPieceSize < n2) cPieceSize = n2 + 1;
|
|
||||||
if (n3 > 0 && cPieceSize < n3) cPieceSize = n3 + 3;
|
|
||||||
// Choose shorter, empty line and \n#
|
|
||||||
if (n4 > 0 && cPieceSize > n4) cPieceSize = n4 + 0;
|
|
||||||
cPieceSize++;
|
|
||||||
}
|
|
||||||
} while (cPieceSize < minimumChunkSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// piece size determined.
|
|
||||||
const piece = leftData.substring(0, cPieceSize);
|
|
||||||
leftData = leftData.substring(cPieceSize);
|
|
||||||
processed++;
|
processed++;
|
||||||
let leafid = "";
|
let leafid = "";
|
||||||
// Get hash of piece.
|
// Get hash of piece.
|
||||||
@@ -646,7 +620,7 @@ export class LocalPouchDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
savenNotes.push(leafid);
|
savenNotes.push(leafid);
|
||||||
} while (leftData != "");
|
}
|
||||||
let saved = true;
|
let saved = true;
|
||||||
if (newLeafs.length > 0) {
|
if (newLeafs.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -727,80 +701,25 @@ export class LocalPouchDB {
|
|||||||
// no op now,
|
// no op now,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
replicateAllToServer(setting: ObsidianLiveSyncSettings, showingNotice?: boolean) {
|
replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean) {
|
||||||
return new Promise(async (res, rej) => {
|
return new Promise(async (res, rej) => {
|
||||||
await this.waitForGCComplete();
|
await this.waitForGCComplete();
|
||||||
this.closeReplication();
|
this.openOneshotReplication(
|
||||||
Logger("send all data to server", LOG_LEVEL.NOTICE);
|
setting,
|
||||||
let notice: Notice = null;
|
showingNotice,
|
||||||
if (showingNotice) {
|
async (e) => {},
|
||||||
notice = new Notice("Initializing", 0);
|
false,
|
||||||
}
|
(e) => {
|
||||||
this.syncStatus = "STARTED";
|
if (e === true) res(e);
|
||||||
this.updateInfo();
|
|
||||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
|
||||||
const auth: Credential = {
|
|
||||||
username: setting.couchDB_USER,
|
|
||||||
password: setting.couchDB_PASSWORD,
|
|
||||||
};
|
|
||||||
const dbret = await connectRemoteCouchDB(uri, auth);
|
|
||||||
if (typeof dbret === "string") {
|
|
||||||
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
|
||||||
if (notice != null) notice.hide();
|
|
||||||
return rej(`could not connect to ${uri}:${dbret}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncOptionBase: PouchDB.Replication.SyncOptions = {
|
|
||||||
pull: {
|
|
||||||
checkpoint: "target",
|
|
||||||
},
|
|
||||||
push: {
|
|
||||||
checkpoint: "source",
|
|
||||||
},
|
|
||||||
batches_limit: setting.batches_limit,
|
|
||||||
batch_size: setting.batch_size,
|
|
||||||
};
|
|
||||||
|
|
||||||
const db = dbret.db;
|
|
||||||
const totalCount = (await this.localDatabase.info()).doc_count;
|
|
||||||
//replicate once
|
|
||||||
const replicate = this.localDatabase.replicate.to(db, { checkpoint: "source", ...syncOptionBase });
|
|
||||||
replicate
|
|
||||||
.on("active", () => {
|
|
||||||
this.syncStatus = "CONNECTED";
|
|
||||||
this.updateInfo();
|
|
||||||
if (notice) {
|
|
||||||
notice.setMessage("CONNECTED");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on("change", (e) => {
|
|
||||||
// no op.
|
|
||||||
this.docSent += e.docs.length;
|
|
||||||
this.updateInfo();
|
|
||||||
notice.setMessage(`SENDING:${e.docs_written}/${totalCount}`);
|
|
||||||
Logger(`replicateAllToServer: sending..:${e.docs.length}`);
|
|
||||||
})
|
|
||||||
.on("complete", (info) => {
|
|
||||||
this.syncStatus = "COMPLETED";
|
|
||||||
this.updateInfo();
|
|
||||||
Logger("replicateAllToServer: Completed", LOG_LEVEL.NOTICE);
|
|
||||||
this.cancelHandler(replicate);
|
|
||||||
if (notice != null) notice.hide();
|
|
||||||
res(true);
|
|
||||||
})
|
|
||||||
.on("error", (e) => {
|
|
||||||
this.syncStatus = "ERRORED";
|
|
||||||
this.updateInfo();
|
|
||||||
Logger("replicateAllToServer: Pulling Replication error", LOG_LEVEL.INFO);
|
|
||||||
Logger(e);
|
|
||||||
this.cancelHandler(replicate);
|
|
||||||
if (notice != null) notice.hide();
|
|
||||||
rej(e);
|
rej(e);
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkReplicationConnectivity(setting: ObsidianLiveSyncSettings, keepAlive: boolean, skipCheck: boolean) {
|
async checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean) {
|
||||||
if (!this.isReady) {
|
if (!this.isReady) {
|
||||||
Logger("Database is not ready.");
|
Logger("Database is not ready.");
|
||||||
return false;
|
return false;
|
||||||
@@ -808,7 +727,7 @@ export class LocalPouchDB {
|
|||||||
|
|
||||||
await this.waitForGCComplete();
|
await this.waitForGCComplete();
|
||||||
if (setting.versionUpFlash != "") {
|
if (setting.versionUpFlash != "") {
|
||||||
new Notice("Open settings and check message, please.");
|
NewNotice("Open settings and check message, please.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
||||||
@@ -820,7 +739,8 @@ export class LocalPouchDB {
|
|||||||
Logger("Another replication running.");
|
Logger("Another replication running.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const dbret = await connectRemoteCouchDB(uri, auth);
|
|
||||||
|
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
|
||||||
if (typeof dbret === "string") {
|
if (typeof dbret === "string") {
|
||||||
Logger(`could not connect to ${uri}: ${dbret}`, LOG_LEVEL.NOTICE);
|
Logger(`could not connect to ${uri}: ${dbret}`, LOG_LEVEL.NOTICE);
|
||||||
return false;
|
return false;
|
||||||
@@ -839,15 +759,6 @@ export class LocalPouchDB {
|
|||||||
locked: false,
|
locked: false,
|
||||||
accepted_nodes: [this.nodeid],
|
accepted_nodes: [this.nodeid],
|
||||||
};
|
};
|
||||||
// const remoteInfo = dbret.info;
|
|
||||||
// const localInfo = await this.localDatabase.info();
|
|
||||||
// const remoteDocsCount = remoteInfo.doc_count;
|
|
||||||
// const localDocsCount = localInfo.doc_count;
|
|
||||||
// const remoteUpdSeq = typeof remoteInfo.update_seq == "string" ? Number(remoteInfo.update_seq.split("-")[0]) : remoteInfo.update_seq;
|
|
||||||
// const localUpdSeq = typeof localInfo.update_seq == "string" ? Number(localInfo.update_seq.split("-")[0]) : localInfo.update_seq;
|
|
||||||
|
|
||||||
// Logger(`Database diffences: remote:${remoteDocsCount} docs / last update ${remoteUpdSeq}`);
|
|
||||||
// Logger(`Database diffences: local :${localDocsCount} docs / last update ${localUpdSeq}`);
|
|
||||||
|
|
||||||
const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint);
|
const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint);
|
||||||
this.remoteLocked = remoteMilestone.locked;
|
this.remoteLocked = remoteMilestone.locked;
|
||||||
@@ -870,191 +781,256 @@ export class LocalPouchDB {
|
|||||||
return { db: dbret.db, info: dbret.info, syncOptionBase, syncOption };
|
return { db: dbret.db, info: dbret.info, syncOptionBase, syncOption };
|
||||||
}
|
}
|
||||||
|
|
||||||
async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>): Promise<boolean> {
|
openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>) {
|
||||||
return await runWithLock("replicate", false, () => {
|
if (keepAlive) {
|
||||||
return this._openReplication(setting, keepAlive, showResult, callback, false);
|
this.openContinuousReplication(setting, showResult, callback, false);
|
||||||
});
|
} else {
|
||||||
|
this.openOneshotReplication(setting, showResult, callback, false, null, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replicationActivated(notice: WrappedNotice) {
|
||||||
|
this.syncStatus = "CONNECTED";
|
||||||
|
this.updateInfo();
|
||||||
|
Logger("Replication activated");
|
||||||
|
if (notice != null) notice.setMessage(`Activated..`);
|
||||||
|
}
|
||||||
|
async replicationChangeDetected(e: PouchDB.Replication.SyncResult<EntryDoc>, notice: WrappedNotice, docSentOnStart: number, docArrivedOnStart: number, callback: ReplicationCallback) {
|
||||||
|
try {
|
||||||
|
if (e.direction == "pull") {
|
||||||
|
await callback(e.change.docs);
|
||||||
|
Logger(`replicated ${e.change.docs_read} doc(s)`);
|
||||||
|
this.docArrived += e.change.docs.length;
|
||||||
|
} else {
|
||||||
|
this.docSent += e.change.docs.length;
|
||||||
|
}
|
||||||
|
if (notice != null) {
|
||||||
|
notice.setMessage(`↑${this.docSent - docSentOnStart} ↓${this.docArrived - docArrivedOnStart}`);
|
||||||
|
}
|
||||||
|
this.updateInfo();
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("Replication callback error", LOG_LEVEL.NOTICE);
|
||||||
|
Logger(ex, LOG_LEVEL.NOTICE);
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replicationCompleted(notice: WrappedNotice, showResult: boolean) {
|
||||||
|
this.syncStatus = "COMPLETED";
|
||||||
|
this.updateInfo();
|
||||||
|
Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||||
|
if (notice != null) notice.hide();
|
||||||
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
|
}
|
||||||
|
replicationDeniend(notice: WrappedNotice, e: any) {
|
||||||
|
this.syncStatus = "ERRORED";
|
||||||
|
this.updateInfo();
|
||||||
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
|
if (notice != null) notice.hide();
|
||||||
|
Logger("Replication denied", LOG_LEVEL.NOTICE);
|
||||||
|
Logger(e);
|
||||||
|
}
|
||||||
|
replicationErrored(notice: WrappedNotice, e: any) {
|
||||||
|
this.syncStatus = "ERRORED";
|
||||||
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
|
this.updateInfo();
|
||||||
|
}
|
||||||
|
replicationPaused(notice: WrappedNotice) {
|
||||||
|
this.syncStatus = "PAUSED";
|
||||||
|
this.updateInfo();
|
||||||
|
if (notice != null) notice.hide();
|
||||||
|
Logger("replication paused", LOG_LEVEL.VERBOSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
originalSetting: ObsidianLiveSyncSettings = null;
|
async openOneshotReplication(
|
||||||
// last_seq: number = 200;
|
setting: RemoteDBSettings,
|
||||||
async _openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>, retrying: boolean): Promise<boolean> {
|
showResult: boolean,
|
||||||
const ret = await this.checkReplicationConnectivity(setting, keepAlive, retrying);
|
callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>,
|
||||||
if (ret === false) return false;
|
retrying: boolean,
|
||||||
let notice: Notice = null;
|
callbackDone: (e: boolean | any) => void,
|
||||||
if (showResult) {
|
pushOnly: boolean,
|
||||||
notice = new Notice("Looking for the point last synchronized point.", 0);
|
pullOnly: boolean
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (this.syncHandler != null) {
|
||||||
|
Logger("Replication is already in progress.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const { db, syncOptionBase, syncOption } = ret;
|
Logger("Oneshot Sync begin...");
|
||||||
//replicate once
|
let thisCallback = callbackDone;
|
||||||
|
const ret = await this.checkReplicationConnectivity(setting, true, retrying);
|
||||||
|
let notice: WrappedNotice = null;
|
||||||
|
if (ret === false) {
|
||||||
|
Logger("Could not connect to server.", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (showResult) {
|
||||||
|
notice = NewNotice("Looking for the point last synchronized point.", 0);
|
||||||
|
}
|
||||||
|
const { db, syncOptionBase } = ret;
|
||||||
this.syncStatus = "STARTED";
|
this.syncStatus = "STARTED";
|
||||||
this.updateInfo();
|
this.updateInfo();
|
||||||
|
|
||||||
let resolved = false;
|
|
||||||
const docArrivedOnStart = this.docArrived;
|
const docArrivedOnStart = this.docArrived;
|
||||||
const docSentOnStart = this.docSent;
|
const docSentOnStart = this.docSent;
|
||||||
|
if (!retrying) {
|
||||||
const _openReplicationSync = () => {
|
// If initial replication, save setting to rollback
|
||||||
Logger("Sync Main Started");
|
this.originalSetting = setting;
|
||||||
if (!retrying) {
|
}
|
||||||
this.originalSetting = setting;
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
}
|
if (!pushOnly && !pullOnly) {
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
this.syncHandler = this.localDatabase.sync(db, { checkpoint: "target", ...syncOptionBase });
|
||||||
this.syncHandler = this.localDatabase.sync<EntryDoc>(db, {
|
|
||||||
...syncOption,
|
|
||||||
pull: {
|
|
||||||
checkpoint: "target",
|
|
||||||
},
|
|
||||||
push: {
|
|
||||||
checkpoint: "source",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.syncHandler
|
this.syncHandler
|
||||||
.on("active", () => {
|
|
||||||
this.syncStatus = "CONNECTED";
|
|
||||||
this.updateInfo();
|
|
||||||
Logger("Replication activated");
|
|
||||||
if (notice != null) notice.setMessage(`Activated..`);
|
|
||||||
})
|
|
||||||
.on("change", async (e) => {
|
.on("change", async (e) => {
|
||||||
try {
|
await this.replicationChangeDetected(e, notice, docSentOnStart, docArrivedOnStart, callback);
|
||||||
if (e.direction == "pull") {
|
|
||||||
// console.log(`pulled data:${e.change.docs.map((e) => e._id).join(",")}`);
|
|
||||||
await callback(e.change.docs);
|
|
||||||
Logger(`replicated ${e.change.docs_read} doc(s)`);
|
|
||||||
this.docArrived += e.change.docs.length;
|
|
||||||
} else {
|
|
||||||
// console.log(`put data:${e.change.docs.map((e) => e._id).join(",")}`);
|
|
||||||
this.docSent += e.change.docs.length;
|
|
||||||
}
|
|
||||||
if (notice != null) {
|
|
||||||
notice.setMessage(`↑${this.docSent - docSentOnStart} ↓${this.docArrived - docArrivedOnStart}`);
|
|
||||||
}
|
|
||||||
this.updateInfo();
|
|
||||||
} catch (ex) {
|
|
||||||
Logger("Replication callback error", LOG_LEVEL.NOTICE);
|
|
||||||
Logger(ex, LOG_LEVEL.NOTICE);
|
|
||||||
}
|
|
||||||
// re-connect to retry with original setting
|
|
||||||
if (retrying) {
|
if (retrying) {
|
||||||
if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) {
|
if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) {
|
||||||
// restore sync values
|
// restore configration.
|
||||||
Logger("Back into original settings once.");
|
Logger("Back into original settings once.");
|
||||||
if (notice != null) notice.hide();
|
if (notice != null) notice.hide();
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
this._openReplication(this.originalSetting, keepAlive, showResult, callback, false);
|
this.openOneshotReplication(this.originalSetting, showResult, callback, false, callbackDone, pushOnly, pullOnly);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("complete", (e) => {
|
.on("complete", (e) => {
|
||||||
this.syncStatus = "COMPLETED";
|
this.replicationCompleted(notice, showResult);
|
||||||
this.updateInfo();
|
if (thisCallback != null) {
|
||||||
Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
thisCallback(true);
|
||||||
if (notice != null) notice.hide();
|
}
|
||||||
if (!keepAlive) {
|
});
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
} else if (pullOnly) {
|
||||||
// if keep alive runnning, resolve here,
|
this.syncHandler = this.localDatabase.replicate.to(db, { checkpoint: "target", ...syncOptionBase });
|
||||||
|
this.syncHandler
|
||||||
|
.on("change", async (e) => {
|
||||||
|
await this.replicationChangeDetected({ direction: "pull", change: e }, notice, docSentOnStart, docArrivedOnStart, callback);
|
||||||
|
if (retrying) {
|
||||||
|
if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) {
|
||||||
|
// restore configration.
|
||||||
|
Logger("Back into original settings once.");
|
||||||
|
if (notice != null) notice.hide();
|
||||||
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
|
this.openOneshotReplication(this.originalSetting, showResult, callback, false, callbackDone, pushOnly, pullOnly);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("denied", (e) => {
|
.on("complete", (e) => {
|
||||||
this.syncStatus = "ERRORED";
|
this.replicationCompleted(notice, showResult);
|
||||||
this.updateInfo();
|
if (thisCallback != null) {
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
thisCallback(true);
|
||||||
if (notice != null) notice.hide();
|
}
|
||||||
Logger("Replication denied", LOG_LEVEL.NOTICE);
|
});
|
||||||
|
} else if (pushOnly) {
|
||||||
|
this.syncHandler = this.localDatabase.replicate.to(db, { checkpoint: "target", ...syncOptionBase });
|
||||||
|
this.syncHandler.on("complete", (e) => {
|
||||||
|
this.replicationCompleted(notice, showResult);
|
||||||
|
if (thisCallback != null) {
|
||||||
|
thisCallback(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncHandler
|
||||||
|
.on("active", () => this.replicationActivated(notice))
|
||||||
|
.on("denied", (e) => {
|
||||||
|
this.replicationDeniend(notice, e);
|
||||||
|
if (thisCallback != null) {
|
||||||
|
thisCallback(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("error", (e) => {
|
||||||
|
this.replicationErrored(notice, e);
|
||||||
|
Logger("Replication stopped.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||||
|
if (notice != null) notice.hide();
|
||||||
|
if (getLastPostFailedBySize()) {
|
||||||
|
// Duplicate settings for smaller batch.
|
||||||
|
const xsetting: RemoteDBSettings = JSON.parse(JSON.stringify(setting));
|
||||||
|
xsetting.batch_size = Math.ceil(xsetting.batch_size / 2) + 2;
|
||||||
|
xsetting.batches_limit = Math.ceil(xsetting.batches_limit / 2) + 2;
|
||||||
|
if (xsetting.batch_size <= 5 && xsetting.batches_limit <= 5) {
|
||||||
|
Logger("We can't replicate more lower value.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||||
|
} else {
|
||||||
|
Logger(`Retry with lower batch size:${xsetting.batch_size}/${xsetting.batches_limit}`, showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||||
|
thisCallback = null;
|
||||||
|
this.openOneshotReplication(xsetting, showResult, callback, true, callbackDone, pushOnly, pullOnly);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger("Replication error", LOG_LEVEL.NOTICE);
|
||||||
Logger(e);
|
Logger(e);
|
||||||
})
|
}
|
||||||
.on("error", (e) => {
|
if (thisCallback != null) {
|
||||||
this.syncStatus = "ERRORED";
|
thisCallback(e);
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
}
|
||||||
this.updateInfo();
|
})
|
||||||
if (notice != null) notice.hide();
|
.on("paused", (e) => this.replicationPaused(notice));
|
||||||
if (getLastPostFailedBySize()) {
|
}
|
||||||
if (keepAlive) {
|
|
||||||
Logger("Replication stopped.", LOG_LEVEL.NOTICE);
|
openContinuousReplication(setting: RemoteDBSettings, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>, retrying: boolean) {
|
||||||
} else {
|
if (this.syncHandler != null) {
|
||||||
// Duplicate settings for smaller batch.
|
Logger("Replication is already in progress.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||||
const xsetting: ObsidianLiveSyncSettings = JSON.parse(JSON.stringify(setting));
|
return;
|
||||||
xsetting.batch_size = Math.ceil(xsetting.batch_size / 2);
|
}
|
||||||
xsetting.batches_limit = Math.ceil(xsetting.batches_limit / 2);
|
Logger("Before LiveSync, start OneShot once...");
|
||||||
if (xsetting.batch_size <= 3 || xsetting.batches_limit <= 3) {
|
this.openOneshotReplication(
|
||||||
Logger("We can't replicate more lower value.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
setting,
|
||||||
} else {
|
showResult,
|
||||||
Logger(`Retry with lower batch size:${xsetting.batch_size}/${xsetting.batches_limit}`, showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
callback,
|
||||||
this._openReplication(xsetting, keepAlive, showResult, callback, true);
|
false,
|
||||||
|
async () => {
|
||||||
|
Logger("LiveSync begin...");
|
||||||
|
const ret = await this.checkReplicationConnectivity(setting, true, true);
|
||||||
|
let notice: WrappedNotice = null;
|
||||||
|
if (ret === false) {
|
||||||
|
Logger("Could not connect to server.", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (showResult) {
|
||||||
|
notice = NewNotice("Looking for the point last synchronized point.", 0);
|
||||||
|
}
|
||||||
|
const { db, syncOption } = ret;
|
||||||
|
this.syncStatus = "STARTED";
|
||||||
|
this.updateInfo();
|
||||||
|
const docArrivedOnStart = this.docArrived;
|
||||||
|
const docSentOnStart = this.docSent;
|
||||||
|
if (!retrying) {
|
||||||
|
//TODO if successfly saven, roll back org setting.
|
||||||
|
this.originalSetting = setting;
|
||||||
|
}
|
||||||
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
|
this.syncHandler = this.localDatabase.sync<EntryDoc>(db, {
|
||||||
|
...syncOption,
|
||||||
|
pull: {
|
||||||
|
checkpoint: "target",
|
||||||
|
},
|
||||||
|
push: {
|
||||||
|
checkpoint: "source",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.syncHandler
|
||||||
|
.on("active", () => this.replicationActivated(notice))
|
||||||
|
.on("change", async (e) => {
|
||||||
|
await this.replicationChangeDetected(e, notice, docSentOnStart, docArrivedOnStart, callback);
|
||||||
|
if (retrying) {
|
||||||
|
if (this.docSent - docSentOnStart + (this.docArrived - docArrivedOnStart) > this.originalSetting.batch_size * 2) {
|
||||||
|
// restore sync values
|
||||||
|
Logger("Back into original settings once.");
|
||||||
|
if (notice != null) notice.hide();
|
||||||
|
this.syncHandler = this.cancelHandler(this.syncHandler);
|
||||||
|
this.openContinuousReplication(this.originalSetting, showResult, callback, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
Logger("Replication error", LOG_LEVEL.NOTICE);
|
.on("complete", (e) => this.replicationCompleted(notice, showResult))
|
||||||
Logger(e);
|
.on("denied", (e) => this.replicationDeniend(notice, e))
|
||||||
}
|
.on("error", (e) => {
|
||||||
})
|
this.replicationErrored(notice, e);
|
||||||
.on("paused", (e) => {
|
Logger("Replication stopped.", LOG_LEVEL.NOTICE);
|
||||||
this.syncStatus = "PAUSED";
|
})
|
||||||
this.updateInfo();
|
.on("paused", (e) => this.replicationPaused(notice));
|
||||||
if (notice != null) notice.hide();
|
},
|
||||||
Logger("replication paused", LOG_LEVEL.VERBOSE);
|
false,
|
||||||
if (keepAlive && !resolved) {
|
true
|
||||||
// if keep alive runnning, resolve here,
|
);
|
||||||
resolved = true;
|
|
||||||
}
|
|
||||||
// Logger(e);
|
|
||||||
});
|
|
||||||
return this.syncHandler;
|
|
||||||
};
|
|
||||||
if (!keepAlive) {
|
|
||||||
await _openReplicationSync();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
|
||||||
Logger("Pull before replicate.");
|
|
||||||
Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE);
|
|
||||||
Logger(await db.info(), LOG_LEVEL.VERBOSE);
|
|
||||||
let replicate: PouchDB.Replication.Replication<EntryDoc>;
|
|
||||||
try {
|
|
||||||
replicate = this.localDatabase.replicate.from(db, { checkpoint: "target", ...syncOptionBase });
|
|
||||||
replicate
|
|
||||||
.on("active", () => {
|
|
||||||
this.syncStatus = "CONNECTED";
|
|
||||||
this.updateInfo();
|
|
||||||
Logger("Replication pull activated.");
|
|
||||||
})
|
|
||||||
.on("change", async (e) => {
|
|
||||||
// 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.
|
|
||||||
// so skip to completed all, we should treat all changes.
|
|
||||||
try {
|
|
||||||
await callback(e.docs);
|
|
||||||
this.docArrived += e.docs.length;
|
|
||||||
this.updateInfo();
|
|
||||||
Logger(`pulled ${e.docs.length} doc(s)`);
|
|
||||||
if (notice != null) {
|
|
||||||
notice.setMessage(`Replication pulled:${e.docs_read}`);
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
Logger("Replication callback error", LOG_LEVEL.NOTICE);
|
|
||||||
Logger(ex, LOG_LEVEL.NOTICE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.syncStatus = "COMPLETED";
|
|
||||||
this.updateInfo();
|
|
||||||
this.cancelHandler(replicate);
|
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
|
||||||
Logger("Replication pull completed.");
|
|
||||||
_openReplicationSync();
|
|
||||||
return true;
|
|
||||||
} catch (ex) {
|
|
||||||
this.syncStatus = "ERRORED";
|
|
||||||
this.updateInfo();
|
|
||||||
Logger("Pulling Replication error:", LOG_LEVEL.NOTICE);
|
|
||||||
Logger(ex, LOG_LEVEL.NOTICE);
|
|
||||||
this.cancelHandler(replicate);
|
|
||||||
this.syncHandler = this.cancelHandler(this.syncHandler);
|
|
||||||
if (notice != null) notice.hide();
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
originalSetting: RemoteDBSettings = null;
|
||||||
|
|
||||||
closeReplication() {
|
closeReplication() {
|
||||||
this.syncStatus = "CLOSED";
|
this.syncStatus = "CLOSED";
|
||||||
this.updateInfo();
|
this.updateInfo();
|
||||||
@@ -1074,14 +1050,14 @@ export class LocalPouchDB {
|
|||||||
this.disposeHashCache();
|
this.disposeHashCache();
|
||||||
Logger("Local Database Reset", LOG_LEVEL.NOTICE);
|
Logger("Local Database Reset", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
async tryResetRemoteDatabase(setting: ObsidianLiveSyncSettings) {
|
async tryResetRemoteDatabase(setting: RemoteDBSettings) {
|
||||||
await this.closeReplication();
|
await this.closeReplication();
|
||||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
||||||
const auth: Credential = {
|
const auth: Credential = {
|
||||||
username: setting.couchDB_USER,
|
username: setting.couchDB_USER,
|
||||||
password: setting.couchDB_PASSWORD,
|
password: setting.couchDB_PASSWORD,
|
||||||
};
|
};
|
||||||
const con = await connectRemoteCouchDB(uri, auth);
|
const con = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
|
||||||
if (typeof con == "string") return;
|
if (typeof con == "string") return;
|
||||||
try {
|
try {
|
||||||
await con.db.destroy();
|
await con.db.destroy();
|
||||||
@@ -1092,24 +1068,24 @@ export class LocalPouchDB {
|
|||||||
Logger(ex, LOG_LEVEL.NOTICE);
|
Logger(ex, LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async tryCreateRemoteDatabase(setting: ObsidianLiveSyncSettings) {
|
async tryCreateRemoteDatabase(setting: RemoteDBSettings) {
|
||||||
await this.closeReplication();
|
await this.closeReplication();
|
||||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
||||||
const auth: Credential = {
|
const auth: Credential = {
|
||||||
username: setting.couchDB_USER,
|
username: setting.couchDB_USER,
|
||||||
password: setting.couchDB_PASSWORD,
|
password: setting.couchDB_PASSWORD,
|
||||||
};
|
};
|
||||||
const con2 = await connectRemoteCouchDB(uri, auth);
|
const con2 = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
|
||||||
if (typeof con2 === "string") return;
|
if (typeof con2 === "string") return;
|
||||||
Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
|
Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
async markRemoteLocked(setting: ObsidianLiveSyncSettings, locked: boolean) {
|
async markRemoteLocked(setting: RemoteDBSettings, locked: boolean) {
|
||||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
||||||
const auth: Credential = {
|
const auth: Credential = {
|
||||||
username: setting.couchDB_USER,
|
username: setting.couchDB_USER,
|
||||||
password: setting.couchDB_PASSWORD,
|
password: setting.couchDB_PASSWORD,
|
||||||
};
|
};
|
||||||
const dbret = await connectRemoteCouchDB(uri, auth);
|
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
|
||||||
if (typeof dbret === "string") {
|
if (typeof dbret === "string") {
|
||||||
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
@@ -1137,13 +1113,13 @@ export class LocalPouchDB {
|
|||||||
}
|
}
|
||||||
await dbret.db.put(remoteMilestone);
|
await dbret.db.put(remoteMilestone);
|
||||||
}
|
}
|
||||||
async markRemoteResolved(setting: ObsidianLiveSyncSettings) {
|
async markRemoteResolved(setting: RemoteDBSettings) {
|
||||||
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME);
|
||||||
const auth: Credential = {
|
const auth: Credential = {
|
||||||
username: setting.couchDB_USER,
|
username: setting.couchDB_USER,
|
||||||
password: setting.couchDB_PASSWORD,
|
password: setting.couchDB_PASSWORD,
|
||||||
};
|
};
|
||||||
const dbret = await connectRemoteCouchDB(uri, auth);
|
const dbret = await connectRemoteCouchDB(uri, auth, setting.disableRequestURI || this.isMobile);
|
||||||
if (typeof dbret === "string") {
|
if (typeof dbret === "string") {
|
||||||
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { App, Modal } from "obsidian";
|
import { App, Modal } from "obsidian";
|
||||||
import { escapeStringToHTML } from "./utils";
|
import { escapeStringToHTML } from "./lib/src/utils";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
export class LogDisplayModal extends Modal {
|
export class LogDisplayModal extends Modal {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { App, Notice, PluginSettingTab, Setting, sanitizeHTMLToDom } from "obsidian";
|
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom } from "obsidian";
|
||||||
import { EntryDoc, LOG_LEVEL } from "./types";
|
import { EntryDoc, LOG_LEVEL } from "./lib/src/types";
|
||||||
import { path2id, id2path, runWithLock } from "./utils";
|
import { path2id, id2path } from "./utils";
|
||||||
import { Logger } from "./logger";
|
import { NewNotice, runWithLock } from "./lib/src/utils";
|
||||||
|
import { Logger } from "./lib/src/logger";
|
||||||
import { connectRemoteCouchDB } from "./utils_couchdb";
|
import { connectRemoteCouchDB } from "./utils_couchdb";
|
||||||
import { testCrypt } from "./e2ee";
|
import { testCrypt } from "./lib/src/e2ee";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||||
@@ -14,10 +15,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
async testConnection(): Promise<void> {
|
async testConnection(): Promise<void> {
|
||||||
const db = await connectRemoteCouchDB(this.plugin.settings.couchDB_URI + (this.plugin.settings.couchDB_DBNAME == "" ? "" : "/" + this.plugin.settings.couchDB_DBNAME), {
|
const db = await connectRemoteCouchDB(
|
||||||
username: this.plugin.settings.couchDB_USER,
|
this.plugin.settings.couchDB_URI + (this.plugin.settings.couchDB_DBNAME == "" ? "" : "/" + this.plugin.settings.couchDB_DBNAME),
|
||||||
password: this.plugin.settings.couchDB_PASSWORD,
|
{
|
||||||
});
|
username: this.plugin.settings.couchDB_USER,
|
||||||
|
password: this.plugin.settings.couchDB_PASSWORD,
|
||||||
|
},
|
||||||
|
this.plugin.settings.disableRequestURI
|
||||||
|
);
|
||||||
if (typeof db === "string") {
|
if (typeof db === "string") {
|
||||||
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE);
|
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
@@ -165,7 +170,19 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.couchDB_DBNAME = value;
|
this.plugin.settings.couchDB_DBNAME = value;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
|
|
||||||
|
new Setting(containerRemoteDatabaseEl)
|
||||||
|
.setDesc("This feature is locked in mobile")
|
||||||
|
.setName("Use the old connecting method")
|
||||||
|
.addToggle((toggle) => {
|
||||||
|
toggle.setValue(this.plugin.settings.disableRequestURI).onChange(async (value) => {
|
||||||
|
this.plugin.settings.disableRequestURI = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
toggle.setDisabled(this.plugin.isMobile);
|
||||||
|
return toggle;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerRemoteDatabaseEl)
|
new Setting(containerRemoteDatabaseEl)
|
||||||
@@ -603,7 +620,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerMiscellaneousEl)
|
new Setting(containerMiscellaneousEl)
|
||||||
.setName("Use history (beta)")
|
.setName("Use history")
|
||||||
.setDesc("Use history dialog (Restart required, auto compaction would be disabled, and more storage will be consumed)")
|
.setDesc("Use history dialog (Restart required, auto compaction would be disabled, and more storage will be consumed)")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.useHistory).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.useHistory).onChange(async (value) => {
|
||||||
@@ -678,7 +695,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
const files = this.app.vault.getFiles();
|
const files = this.app.vault.getFiles();
|
||||||
Logger("Verify and repair all files started", LOG_LEVEL.NOTICE);
|
Logger("Verify and repair all files started", LOG_LEVEL.NOTICE);
|
||||||
const notice = new Notice("", 0);
|
const notice = NewNotice("", 0);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
i++;
|
i++;
|
||||||
@@ -704,7 +721,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.setWarning()
|
.setWarning()
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
const notice = new Notice("", 0);
|
const notice = NewNotice("", 0);
|
||||||
Logger(`Begin sanity check`, LOG_LEVEL.INFO);
|
Logger(`Begin sanity check`, LOG_LEVEL.INFO);
|
||||||
notice.setMessage(`Begin sanity check`);
|
notice.setMessage(`Begin sanity check`);
|
||||||
await runWithLock("sancheck", true, async () => {
|
await runWithLock("sancheck", true, async () => {
|
||||||
@@ -824,7 +841,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const updateDisabledOfDeviceAndVaultName = () => {
|
const updateDisabledOfDeviceAndVaultName = () => {
|
||||||
vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic);
|
vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic);
|
||||||
vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto sweep." : "");
|
vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto scan." : "");
|
||||||
};
|
};
|
||||||
new Setting(containerPluginSettings).setName("Enable plugin synchronization").addToggle((toggle) =>
|
new Setting(containerPluginSettings).setName("Enable plugin synchronization").addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
|
||||||
@@ -832,16 +849,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
new Setting(containerPluginSettings).setName("Show own plugins and settings").addToggle((toggle) =>
|
|
||||||
toggle.setValue(this.plugin.settings.showOwnPlugins).onChange(async (value) => {
|
|
||||||
this.plugin.settings.showOwnPlugins = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Sweep plugins automatically")
|
.setName("Scan plugins automatically")
|
||||||
.setDesc("Sweep plugins before replicating.")
|
.setDesc("Scan plugins before replicating.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => {
|
||||||
this.plugin.settings.autoSweepPlugins = value;
|
this.plugin.settings.autoSweepPlugins = value;
|
||||||
@@ -851,8 +862,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerPluginSettings)
|
new Setting(containerPluginSettings)
|
||||||
.setName("Sweep plugins periodically")
|
.setName("Scan plugins periodically")
|
||||||
.setDesc("Sweep plugins each 1 minutes.")
|
.setDesc("Scan plugins each 1 minutes.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => {
|
||||||
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
this.plugin.settings.autoSweepPluginsPeriodic = value;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { DevicePluginList, PluginDataEntry } from "./types";
|
import { DevicePluginList, PluginDataEntry } from "./types";
|
||||||
import { versionNumberString2Number } from "./utils";
|
import { versionNumberString2Number } from "./lib/src/utils";
|
||||||
|
|
||||||
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";
|
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@
|
|||||||
|
|
||||||
<div class="ols-plugins-div-buttons">
|
<div class="ols-plugins-div-buttons">
|
||||||
<button class="mod-cta" on:click={checkUpdates}>Check Updates</button>
|
<button class="mod-cta" on:click={checkUpdates}>Check Updates</button>
|
||||||
<button class="mod-cta" on:click={sweepPlugins}>Sweep installed</button>
|
<button class="mod-cta" on:click={sweepPlugins}>Scan installed</button>
|
||||||
<button class="mod-cta" on:click={applyPlugins}>Apply all</button>
|
<button class="mod-cta" on:click={applyPlugins}>Apply all</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="ols-plugins-div-buttons">-->
|
<!-- <div class="ols-plugins-div-buttons">-->
|
||||||
|
|||||||
168
src/e2ee.ts
168
src/e2ee.ts
@@ -1,168 +0,0 @@
|
|||||||
import { Logger } from "./logger";
|
|
||||||
import { LOG_LEVEL } from "./types";
|
|
||||||
|
|
||||||
export type encodedData = [encryptedData: string, iv: string, salt: string];
|
|
||||||
export type KeyBuffer = {
|
|
||||||
index: string;
|
|
||||||
key: CryptoKey;
|
|
||||||
salt: Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
const KeyBuffs: KeyBuffer[] = [];
|
|
||||||
const decKeyBuffs: KeyBuffer[] = [];
|
|
||||||
|
|
||||||
const KEY_RECYCLE_COUNT = 100;
|
|
||||||
let recycleCount = KEY_RECYCLE_COUNT;
|
|
||||||
|
|
||||||
let semiStaticFieldBuffer: Uint8Array = null;
|
|
||||||
const nonceBuffer: Uint32Array = new Uint32Array(1);
|
|
||||||
|
|
||||||
export async function getKeyForEncrypt(passphrase: string): Promise<[CryptoKey, Uint8Array]> {
|
|
||||||
// For performance, the plugin reuses the key KEY_RECYCLE_COUNT times.
|
|
||||||
const f = KeyBuffs.find((e) => e.index == passphrase);
|
|
||||||
if (f) {
|
|
||||||
recycleCount--;
|
|
||||||
if (recycleCount > 0) {
|
|
||||||
return [f.key, f.salt];
|
|
||||||
}
|
|
||||||
KeyBuffs.remove(f);
|
|
||||||
recycleCount = KEY_RECYCLE_COUNT;
|
|
||||||
}
|
|
||||||
const xpassphrase = new TextEncoder().encode(passphrase);
|
|
||||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase);
|
|
||||||
const keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]);
|
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
||||||
const key = await crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: "PBKDF2",
|
|
||||||
salt,
|
|
||||||
iterations: 100000,
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
keyMaterial,
|
|
||||||
{ name: "AES-GCM", length: 256 },
|
|
||||||
false,
|
|
||||||
["encrypt"]
|
|
||||||
);
|
|
||||||
KeyBuffs.push({
|
|
||||||
index: passphrase,
|
|
||||||
key,
|
|
||||||
salt,
|
|
||||||
});
|
|
||||||
while (KeyBuffs.length > 50) {
|
|
||||||
KeyBuffs.shift();
|
|
||||||
}
|
|
||||||
return [key, salt];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getKeyForDecryption(passphrase: string, salt: Uint8Array): Promise<[CryptoKey, Uint8Array]> {
|
|
||||||
const bufKey = passphrase + uint8ArrayToHexString(salt);
|
|
||||||
const f = decKeyBuffs.find((e) => e.index == bufKey);
|
|
||||||
if (f) {
|
|
||||||
return [f.key, f.salt];
|
|
||||||
}
|
|
||||||
const xpassphrase = new TextEncoder().encode(passphrase);
|
|
||||||
const digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase);
|
|
||||||
const keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]);
|
|
||||||
const key = await crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: "PBKDF2",
|
|
||||||
salt,
|
|
||||||
iterations: 100000,
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
keyMaterial,
|
|
||||||
{ name: "AES-GCM", length: 256 },
|
|
||||||
false,
|
|
||||||
["decrypt"]
|
|
||||||
);
|
|
||||||
decKeyBuffs.push({
|
|
||||||
index: bufKey,
|
|
||||||
key,
|
|
||||||
salt,
|
|
||||||
});
|
|
||||||
while (decKeyBuffs.length > 50) {
|
|
||||||
decKeyBuffs.shift();
|
|
||||||
}
|
|
||||||
return [key, salt];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSemiStaticField(reset?: boolean) {
|
|
||||||
// return fixed field of iv.
|
|
||||||
if (semiStaticFieldBuffer != null && !reset) {
|
|
||||||
return semiStaticFieldBuffer;
|
|
||||||
}
|
|
||||||
semiStaticFieldBuffer = crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
return semiStaticFieldBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNonce() {
|
|
||||||
// This is nonce, so do not send same thing.
|
|
||||||
nonceBuffer[0]++;
|
|
||||||
if (nonceBuffer[0] > 10000) {
|
|
||||||
// reset semi-static field.
|
|
||||||
getSemiStaticField(true);
|
|
||||||
}
|
|
||||||
return nonceBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function uint8ArrayToHexString(src: Uint8Array): string {
|
|
||||||
return Array.from(src)
|
|
||||||
.map((e: number): string => `00${e.toString(16)}`.slice(-2))
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
function hexStringToUint8Array(src: string): Uint8Array {
|
|
||||||
const srcArr = [...src];
|
|
||||||
const arr = srcArr.reduce((acc, _, i) => (i % 2 ? acc : [...acc, srcArr.slice(i, i + 2).join("")]), []).map((e) => parseInt(e, 16));
|
|
||||||
return Uint8Array.from(arr);
|
|
||||||
}
|
|
||||||
export async function encrypt(input: string, passphrase: string) {
|
|
||||||
const [key, salt] = await getKeyForEncrypt(passphrase);
|
|
||||||
// Create initial vector with semifixed part and incremental part
|
|
||||||
// I think it's not good against related-key attacks.
|
|
||||||
const fixedPart = getSemiStaticField();
|
|
||||||
const invocationPart = getNonce();
|
|
||||||
const iv = Uint8Array.from([...fixedPart, ...new Uint8Array(invocationPart.buffer)]);
|
|
||||||
const plainStringified: string = JSON.stringify(input);
|
|
||||||
const plainStringBuffer: Uint8Array = new TextEncoder().encode(plainStringified);
|
|
||||||
const encryptedDataArrayBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plainStringBuffer);
|
|
||||||
|
|
||||||
const encryptedData = window.btoa(Array.from(new Uint8Array(encryptedDataArrayBuffer), (char) => String.fromCharCode(char)).join(""));
|
|
||||||
|
|
||||||
//return data with iv and salt.
|
|
||||||
const response: encodedData = [encryptedData, uint8ArrayToHexString(iv), uint8ArrayToHexString(salt)];
|
|
||||||
const ret = JSON.stringify(response);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function decrypt(encryptedResult: string, passphrase: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const [encryptedData, ivString, salt]: encodedData = JSON.parse(encryptedResult);
|
|
||||||
const [key] = await getKeyForDecryption(passphrase, hexStringToUint8Array(salt));
|
|
||||||
const iv = hexStringToUint8Array(ivString);
|
|
||||||
// decode base 64, it should increase speed and i should with in MAX_DOC_SIZE_BIN, so it won't OOM.
|
|
||||||
const encryptedDataBin = window.atob(encryptedData);
|
|
||||||
const encryptedDataArrayBuffer = Uint8Array.from(encryptedDataBin.split(""), (char) => char.charCodeAt(0));
|
|
||||||
const plainStringBuffer: ArrayBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encryptedDataArrayBuffer);
|
|
||||||
const plainStringified = new TextDecoder().decode(plainStringBuffer);
|
|
||||||
const plain = JSON.parse(plainStringified);
|
|
||||||
return plain;
|
|
||||||
} catch (ex) {
|
|
||||||
Logger("Couldn't decode! You should wrong the passphrases", LOG_LEVEL.VERBOSE);
|
|
||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function testCrypt() {
|
|
||||||
const src = "supercalifragilisticexpialidocious";
|
|
||||||
const encoded = await encrypt(src, "passwordTest");
|
|
||||||
const decrypted = await decrypt(encoded, "passwordTest");
|
|
||||||
if (src != decrypted) {
|
|
||||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.VERBOSE);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
Logger("CRYPT LOGIC OK", LOG_LEVEL.VERBOSE);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
src/lib
Submodule
1
src/lib
Submodule
Submodule src/lib added at d495f4577a
@@ -1,13 +0,0 @@
|
|||||||
import { LOG_LEVEL } from "./types";
|
|
||||||
|
|
||||||
// eslint-disable-next-line require-await
|
|
||||||
export let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message, _) => {
|
|
||||||
const timestamp = new Date().toLocaleString();
|
|
||||||
const messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
|
||||||
const newmessage = timestamp + "->" + messagecontent;
|
|
||||||
console.log(newmessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setLogger(loggerFun: (message: any, levlel?: LOG_LEVEL) => Promise<void>) {
|
|
||||||
Logger = loggerFun;
|
|
||||||
}
|
|
||||||
137
src/main.ts
137
src/main.ts
@@ -1,25 +1,24 @@
|
|||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App } from "obsidian";
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App } from "obsidian";
|
||||||
import { diff_match_patch } from "diff-match-patch";
|
import { diff_match_patch } from "diff-match-patch";
|
||||||
|
|
||||||
|
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG } from "./lib/src/types";
|
||||||
|
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList } from "./types";
|
||||||
import {
|
import {
|
||||||
EntryDoc,
|
base64ToString,
|
||||||
LoadedEntry,
|
arrayBufferToBase64,
|
||||||
ObsidianLiveSyncSettings,
|
base64ToArrayBuffer,
|
||||||
diff_check_result,
|
isValidPath,
|
||||||
diff_result_leaf,
|
versionNumberString2Number,
|
||||||
EntryBody,
|
runWithLock,
|
||||||
PluginDataEntry,
|
shouldBeIgnored,
|
||||||
LOG_LEVEL,
|
getProcessingCounts,
|
||||||
VER,
|
setLockNotifier,
|
||||||
PERIODIC_PLUGIN_SWEEP,
|
isPlainText,
|
||||||
DEFAULT_SETTINGS,
|
setNoticeClass,
|
||||||
PluginList,
|
NewNotice,
|
||||||
DevicePluginList,
|
allSettledWithConcurrencyLimit,
|
||||||
diff_result,
|
} from "./lib/src/utils";
|
||||||
FLAGMD_REDFLAG,
|
import { Logger, setLogger } from "./lib/src/logger";
|
||||||
} from "./types";
|
|
||||||
import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock, shouldBeIgnored, getProcessingCounts, setLockNotifier, isPlainText } from "./utils";
|
|
||||||
import { Logger, setLogger } from "./logger";
|
|
||||||
import { LocalPouchDB } from "./LocalPouchDB";
|
import { LocalPouchDB } from "./LocalPouchDB";
|
||||||
import { LogDisplayModal } from "./LogDisplayModal";
|
import { LogDisplayModal } from "./LogDisplayModal";
|
||||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||||
@@ -27,7 +26,8 @@ import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
|||||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||||
|
|
||||||
import PluginPane from "./PluginPane.svelte";
|
import PluginPane from "./PluginPane.svelte";
|
||||||
|
import { id2path, path2id } from "./utils";
|
||||||
|
setNoticeClass(Notice);
|
||||||
class PluginDialogModal extends Modal {
|
class PluginDialogModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
logEl: HTMLDivElement;
|
logEl: HTMLDivElement;
|
||||||
@@ -64,6 +64,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
statusBar2: HTMLElement;
|
statusBar2: HTMLElement;
|
||||||
suspended: boolean;
|
suspended: boolean;
|
||||||
deviceAndVaultName: string;
|
deviceAndVaultName: string;
|
||||||
|
isMobile = false;
|
||||||
|
|
||||||
setInterval(handler: () => any, timeout?: number): number {
|
setInterval(handler: () => any, timeout?: number): number {
|
||||||
const timer = window.setInterval(handler, timeout);
|
const timer = window.setInterval(handler, timeout);
|
||||||
@@ -93,6 +94,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName();
|
const lsname = "obsidian-live-sync-ver" + this.app.vault.getName();
|
||||||
const last_version = localStorage.getItem(lsname);
|
const last_version = localStorage.getItem(lsname);
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
//@ts-ignore
|
||||||
|
if (this.app.isMobile) {
|
||||||
|
this.isMobile = true;
|
||||||
|
this.settings.disableRequestURI = true;
|
||||||
|
}
|
||||||
if (last_version && Number(last_version) < VER) {
|
if (last_version && Number(last_version) < VER) {
|
||||||
this.settings.liveSync = false;
|
this.settings.liveSync = false;
|
||||||
this.settings.syncOnSave = false;
|
this.settings.syncOnSave = false;
|
||||||
@@ -180,7 +186,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.realizeSettingSyncMode();
|
await this.realizeSettingSyncMode();
|
||||||
this.registerWatchEvents();
|
this.registerWatchEvents();
|
||||||
if (this.settings.syncOnStart) {
|
if (this.settings.syncOnStart) {
|
||||||
await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE);
|
Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE);
|
||||||
@@ -190,8 +196,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-replicate",
|
id: "livesync-replicate",
|
||||||
name: "Replicate now",
|
name: "Replicate now",
|
||||||
callback: () => {
|
callback: async () => {
|
||||||
this.replicate();
|
await this.replicate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
@@ -306,7 +312,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
const vaultName = this.app.vault.getName();
|
const vaultName = this.app.vault.getName();
|
||||||
Logger("Open Database...");
|
Logger("Open Database...");
|
||||||
this.localDatabase = new LocalPouchDB(this.settings, vaultName);
|
//@ts-ignore
|
||||||
|
const isMobile = this.app.isMobile;
|
||||||
|
this.localDatabase = new LocalPouchDB(this.settings, vaultName, isMobile);
|
||||||
this.localDatabase.updateInfo = () => {
|
this.localDatabase.updateInfo = () => {
|
||||||
this.refreshStatusText();
|
this.refreshStatusText();
|
||||||
};
|
};
|
||||||
@@ -365,7 +373,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
|
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
|
||||||
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
||||||
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
||||||
this.registerEvent(this.app.vault.on("create", this.watchVaultChange));
|
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
|
||||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||||
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
|
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
|
||||||
}
|
}
|
||||||
@@ -389,10 +397,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
if (this.settings.liveSync) {
|
if (this.settings.liveSync) {
|
||||||
await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
if (this.settings.syncOnStart) {
|
if (this.settings.syncOnStart) {
|
||||||
await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
if (this.settings.periodicReplication) {
|
if (this.settings.periodicReplication) {
|
||||||
this.setPeriodicSync();
|
this.setPeriodicSync();
|
||||||
@@ -408,7 +416,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
async watchWorkspaceOpenAsync(file: TFile) {
|
async watchWorkspaceOpenAsync(file: TFile) {
|
||||||
await this.applyBatchChange();
|
await this.applyBatchChange();
|
||||||
if (file == null) return;
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.settings.syncOnFileOpen && !this.suspended) {
|
if (this.settings.syncOnFileOpen && !this.suspended) {
|
||||||
await this.replicate();
|
await this.replicate();
|
||||||
}
|
}
|
||||||
@@ -427,10 +437,21 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
|
|
||||||
// If batchsave is enabled, queue all changes and do nothing.
|
// If batchsave is enabled, queue all changes and do nothing.
|
||||||
if (this.settings.batchSave) {
|
if (this.settings.batchSave) {
|
||||||
this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path]));
|
~(async () => {
|
||||||
this.refreshStatusText();
|
const meta = await this.localDatabase.getDBEntryMeta(file.path);
|
||||||
|
if (meta != false) {
|
||||||
|
const localMtime = ~~(file.stat.mtime / 1000);
|
||||||
|
const docMtime = ~~(meta.mtime / 1000);
|
||||||
|
if (localMtime !== docMtime) {
|
||||||
|
// Perhaps we have to modify (to using newer doc), but we don't be sure to every device's clock is adjusted.
|
||||||
|
this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path]));
|
||||||
|
this.refreshStatusText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.watchVaultChangeAsync(file, ...args);
|
this.watchVaultChangeAsync(file, ...args);
|
||||||
@@ -438,7 +459,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
async applyBatchChange() {
|
async applyBatchChange() {
|
||||||
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
|
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
|
||||||
return [];
|
return;
|
||||||
}
|
}
|
||||||
return await runWithLock("batchSave", false, async () => {
|
return await runWithLock("batchSave", false, async () => {
|
||||||
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
||||||
@@ -456,7 +477,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.refreshStatusText();
|
this.refreshStatusText();
|
||||||
return await Promise.all(promises);
|
await allSettledWithConcurrencyLimit(promises, 3);
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,7 +713,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
|
async doc2storage_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
|
||||||
const pathSrc = id2path(docEntry._id);
|
const pathSrc = id2path(docEntry._id);
|
||||||
if (shouldBeIgnored(pathSrc)) {
|
if (shouldBeIgnored(pathSrc)) {
|
||||||
return;
|
return;
|
||||||
@@ -770,7 +792,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
} else if (targetFile instanceof TFile) {
|
} else if (targetFile instanceof TFile) {
|
||||||
const doc = change;
|
const doc = change;
|
||||||
const file = targetFile;
|
const file = targetFile;
|
||||||
await this.doc2storate_modify(doc, file);
|
await this.doc2storage_modify(doc, file);
|
||||||
this.queueConflictedCheck(file);
|
this.queueConflictedCheck(file);
|
||||||
} else {
|
} else {
|
||||||
Logger(`${id2path(change._id)} is already exist as the folder`);
|
Logger(`${id2path(change._id)} is already exist as the folder`);
|
||||||
@@ -836,7 +858,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
a.addEventListener("click", () => this.showPluginSyncModal());
|
a.addEventListener("click", () => this.showPluginSyncModal());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
new Notice(fragment, 10000);
|
NewNotice(fragment, 10000);
|
||||||
} else {
|
} else {
|
||||||
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
@@ -891,7 +913,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
if (this.settings.liveSync) {
|
if (this.settings.liveSync) {
|
||||||
await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
||||||
this.refreshStatusText();
|
this.refreshStatusText();
|
||||||
}
|
}
|
||||||
this.setPeriodicSync();
|
this.setPeriodicSync();
|
||||||
@@ -953,14 +975,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
async replicate(showMessage?: boolean) {
|
async replicate(showMessage?: boolean) {
|
||||||
if (this.settings.versionUpFlash != "") {
|
if (this.settings.versionUpFlash != "") {
|
||||||
new Notice("Open settings and check message, please.");
|
NewNotice("Open settings and check message, please.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.applyBatchChange();
|
await this.applyBatchChange();
|
||||||
if (this.settings.autoSweepPlugins) {
|
if (this.settings.autoSweepPlugins) {
|
||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
await this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeDatabase(showingNotice?: boolean) {
|
async initializeDatabase(showingNotice?: boolean) {
|
||||||
@@ -991,7 +1013,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
// synchronize all files between database and storage.
|
// synchronize all files between database and storage.
|
||||||
let notice: Notice = null;
|
let notice: Notice = null;
|
||||||
if (showingNotice) {
|
if (showingNotice) {
|
||||||
notice = new Notice("Initializing", 0);
|
notice = NewNotice("Initializing", 0);
|
||||||
}
|
}
|
||||||
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);
|
||||||
@@ -1013,12 +1035,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger(procedurename);
|
Logger(procedurename);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
// let lastTicks = performance.now() + 2000;
|
// let lastTicks = performance.now() + 2000;
|
||||||
|
let workProcs = 0;
|
||||||
const procs = objects.map(async (e) => {
|
const procs = objects.map(async (e) => {
|
||||||
try {
|
try {
|
||||||
|
workProcs++;
|
||||||
await callback(e);
|
await callback(e);
|
||||||
i++;
|
i++;
|
||||||
if (i % 25 == 0) {
|
if (i % 25 == 0) {
|
||||||
const notify = `${procedurename} : ${i}/${count}`;
|
const notify = `${procedurename} : ${workProcs}/${count} (Pending:${workProcs})`;
|
||||||
if (notice != null) notice.setMessage(notify);
|
if (notice != null) notice.setMessage(notify);
|
||||||
Logger(notify);
|
Logger(notify);
|
||||||
this.setStatusBarText(notify);
|
this.setStatusBarText(notify);
|
||||||
@@ -1026,27 +1050,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE);
|
Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE);
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
|
} finally {
|
||||||
|
workProcs--;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
|
||||||
if (!Promise.allSettled) {
|
await allSettledWithConcurrencyLimit(procs, 10);
|
||||||
await Promise.all(
|
|
||||||
procs.map((p) =>
|
|
||||||
p
|
|
||||||
.then((value) => ({
|
|
||||||
status: "fulfilled",
|
|
||||||
value,
|
|
||||||
}))
|
|
||||||
.catch((reason) => ({
|
|
||||||
status: "rejected",
|
|
||||||
reason,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
await Promise.allSettled(procs);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
||||||
Logger(`Update into ${e.path}`);
|
Logger(`Update into ${e.path}`);
|
||||||
@@ -1232,10 +1241,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
//concat both,
|
//concat both,
|
||||||
// write data,and delete both old rev.
|
// write data,and delete both old rev.
|
||||||
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
|
||||||
await this.app.vault.modify(file, p);
|
|
||||||
await this.updateIntoDB(file);
|
|
||||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev });
|
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev });
|
||||||
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev });
|
await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev });
|
||||||
|
await this.app.vault.modify(file, p);
|
||||||
|
await this.updateIntoDB(file);
|
||||||
await this.pullFile(file.path);
|
await this.pullFile(file.path);
|
||||||
Logger("concat both file");
|
Logger("concat both file");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1318,7 +1327,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const file = targetFile;
|
const file = targetFile;
|
||||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||||
if (doc === false) return;
|
if (doc === false) return;
|
||||||
await this.doc2storate_modify(doc, file, force);
|
await this.doc2storage_modify(doc, file, force);
|
||||||
} else {
|
} else {
|
||||||
Logger(`target files:${filename} is exists as the folder`);
|
Logger(`target files:${filename} is exists as the folder`);
|
||||||
//something went wrong..
|
//something went wrong..
|
||||||
@@ -1343,7 +1352,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger(`${storageMtime} < ${docMtime}`);
|
Logger(`${storageMtime} < ${docMtime}`);
|
||||||
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
||||||
if (docx != false) {
|
if (docx != false) {
|
||||||
await this.doc2storate_modify(docx, file);
|
await this.doc2storage_modify(docx, file);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE);
|
// Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE);
|
||||||
@@ -1460,7 +1469,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Logger("Sweeping plugins", logLevel);
|
Logger("Scanning plugins", logLevel);
|
||||||
const db = this.localDatabase.localDatabase;
|
const db = this.localDatabase.localDatabase;
|
||||||
const oldDocs = await db.allDocs({
|
const oldDocs = await db.allDocs({
|
||||||
startkey: `ps:${this.deviceAndVaultName}-`,
|
startkey: `ps:${this.deviceAndVaultName}-`,
|
||||||
@@ -1532,7 +1541,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
return e.doc;
|
return e.doc;
|
||||||
});
|
});
|
||||||
await db.bulkDocs(delDocs);
|
await db.bulkDocs(delDocs);
|
||||||
Logger(`Sweep plugin done.`, logLevel);
|
Logger(`Scan plugin done.`, logLevel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/pouchdb-browser.ts
Normal file
4
src/pouchdb-browser.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PouchDB as PouchDB_ } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
||||||
|
|
||||||
|
const Pouch: PouchDB.Static = PouchDB_;
|
||||||
|
export { Pouch as PouchDB };
|
||||||
215
src/types.ts
215
src/types.ts
@@ -1,152 +1,7 @@
|
|||||||
// docs should be encoded as base64, so 1 char -> 1 bytes
|
|
||||||
// and cloudant limitation is 1MB , we use 900kb;
|
|
||||||
|
|
||||||
import { PluginManifest } from "obsidian";
|
import { PluginManifest } from "obsidian";
|
||||||
import * as PouchDB from "pouchdb";
|
import { DatabaseEntry } from "./lib/src/types";
|
||||||
|
|
||||||
export const MAX_DOC_SIZE = 1000; // for .md file, but if delimiters exists. use that before.
|
export interface PluginDataEntry extends DatabaseEntry {
|
||||||
export const MAX_DOC_SIZE_BIN = 102400; // 100kb
|
|
||||||
export const VER = 10;
|
|
||||||
|
|
||||||
export const RECENT_MOFIDIED_DOCS_QTY = 30;
|
|
||||||
export const LEAF_WAIT_TIMEOUT = 90000; // in synchronization, waiting missing leaf time out.
|
|
||||||
export const LOG_LEVEL = {
|
|
||||||
VERBOSE: 1,
|
|
||||||
INFO: 10,
|
|
||||||
NOTICE: 100,
|
|
||||||
URGENT: 1000,
|
|
||||||
} as const;
|
|
||||||
export type LOG_LEVEL = typeof LOG_LEVEL[keyof typeof LOG_LEVEL];
|
|
||||||
export const VERSIONINFO_DOCID = "obsydian_livesync_version";
|
|
||||||
export const MILSTONE_DOCID = "_local/obsydian_livesync_milestone";
|
|
||||||
export const NODEINFO_DOCID = "_local/obsydian_livesync_nodeinfo";
|
|
||||||
|
|
||||||
export interface ObsidianLiveSyncSettings {
|
|
||||||
couchDB_URI: string;
|
|
||||||
couchDB_USER: string;
|
|
||||||
couchDB_PASSWORD: string;
|
|
||||||
couchDB_DBNAME: string;
|
|
||||||
liveSync: boolean;
|
|
||||||
syncOnSave: boolean;
|
|
||||||
syncOnStart: boolean;
|
|
||||||
syncOnFileOpen: boolean;
|
|
||||||
savingDelay: number;
|
|
||||||
lessInformationInLog: boolean;
|
|
||||||
gcDelay: number;
|
|
||||||
versionUpFlash: string;
|
|
||||||
minimumChunkSize: number;
|
|
||||||
longLineThreshold: number;
|
|
||||||
showVerboseLog: boolean;
|
|
||||||
suspendFileWatching: boolean;
|
|
||||||
trashInsteadDelete: boolean;
|
|
||||||
periodicReplication: boolean;
|
|
||||||
periodicReplicationInterval: number;
|
|
||||||
encrypt: boolean;
|
|
||||||
passphrase: string;
|
|
||||||
workingEncrypt: boolean;
|
|
||||||
workingPassphrase: string;
|
|
||||||
doNotDeleteFolder: boolean;
|
|
||||||
resolveConflictsByNewerFile: boolean;
|
|
||||||
batchSave: boolean;
|
|
||||||
deviceAndVaultName: string;
|
|
||||||
usePluginSettings: boolean;
|
|
||||||
showOwnPlugins: boolean;
|
|
||||||
showStatusOnEditor: boolean;
|
|
||||||
usePluginSync: boolean;
|
|
||||||
autoSweepPlugins: boolean;
|
|
||||||
autoSweepPluginsPeriodic: boolean;
|
|
||||||
notifyPluginOrSettingUpdated: boolean;
|
|
||||||
checkIntegrityOnSave: boolean;
|
|
||||||
batch_size: number;
|
|
||||||
batches_limit: number;
|
|
||||||
useHistory: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
|
||||||
couchDB_URI: "",
|
|
||||||
couchDB_USER: "",
|
|
||||||
couchDB_PASSWORD: "",
|
|
||||||
couchDB_DBNAME: "",
|
|
||||||
liveSync: false,
|
|
||||||
syncOnSave: false,
|
|
||||||
syncOnStart: false,
|
|
||||||
savingDelay: 200,
|
|
||||||
lessInformationInLog: false,
|
|
||||||
gcDelay: 300,
|
|
||||||
versionUpFlash: "",
|
|
||||||
minimumChunkSize: 20,
|
|
||||||
longLineThreshold: 250,
|
|
||||||
showVerboseLog: false,
|
|
||||||
suspendFileWatching: false,
|
|
||||||
trashInsteadDelete: true,
|
|
||||||
periodicReplication: false,
|
|
||||||
periodicReplicationInterval: 60,
|
|
||||||
syncOnFileOpen: false,
|
|
||||||
encrypt: false,
|
|
||||||
passphrase: "",
|
|
||||||
workingEncrypt: false,
|
|
||||||
workingPassphrase: "",
|
|
||||||
doNotDeleteFolder: false,
|
|
||||||
resolveConflictsByNewerFile: false,
|
|
||||||
batchSave: false,
|
|
||||||
deviceAndVaultName: "",
|
|
||||||
usePluginSettings: false,
|
|
||||||
showOwnPlugins: false,
|
|
||||||
showStatusOnEditor: false,
|
|
||||||
usePluginSync: false,
|
|
||||||
autoSweepPlugins: false,
|
|
||||||
autoSweepPluginsPeriodic: false,
|
|
||||||
notifyPluginOrSettingUpdated: false,
|
|
||||||
checkIntegrityOnSave: false,
|
|
||||||
batch_size: 250,
|
|
||||||
batches_limit: 40,
|
|
||||||
useHistory: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PERIODIC_PLUGIN_SWEEP = 60;
|
|
||||||
|
|
||||||
export interface Entry {
|
|
||||||
_id: string;
|
|
||||||
data: string;
|
|
||||||
_rev?: string;
|
|
||||||
ctime: number;
|
|
||||||
mtime: number;
|
|
||||||
size: number;
|
|
||||||
_deleted?: boolean;
|
|
||||||
_conflicts?: string[];
|
|
||||||
type?: "notes";
|
|
||||||
}
|
|
||||||
export interface NewEntry {
|
|
||||||
_id: string;
|
|
||||||
children: string[];
|
|
||||||
_rev?: string;
|
|
||||||
ctime: number;
|
|
||||||
mtime: number;
|
|
||||||
size: number;
|
|
||||||
_deleted?: boolean;
|
|
||||||
_conflicts?: string[];
|
|
||||||
NewNote: true;
|
|
||||||
type: "newnote";
|
|
||||||
}
|
|
||||||
export interface PlainEntry {
|
|
||||||
_id: string;
|
|
||||||
children: string[];
|
|
||||||
_rev?: string;
|
|
||||||
ctime: number;
|
|
||||||
mtime: number;
|
|
||||||
size: number;
|
|
||||||
_deleted?: boolean;
|
|
||||||
NewNote: true;
|
|
||||||
_conflicts?: string[];
|
|
||||||
type: "plain";
|
|
||||||
}
|
|
||||||
export type LoadedEntry = Entry & {
|
|
||||||
children: string[];
|
|
||||||
datatype: "plain" | "newnote";
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PluginDataEntry {
|
|
||||||
_id: string;
|
|
||||||
deviceVaultName: string;
|
deviceVaultName: string;
|
||||||
mtime: number;
|
mtime: number;
|
||||||
manifest: PluginManifest;
|
manifest: PluginManifest;
|
||||||
@@ -155,73 +10,10 @@ export interface PluginDataEntry {
|
|||||||
styleCss?: string;
|
styleCss?: string;
|
||||||
// it must be encrypted.
|
// it must be encrypted.
|
||||||
dataJson?: string;
|
dataJson?: string;
|
||||||
_rev?: string;
|
|
||||||
_deleted?: boolean;
|
|
||||||
_conflicts?: string[];
|
_conflicts?: string[];
|
||||||
type: "plugin";
|
type: "plugin";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntryLeaf {
|
|
||||||
_id: string;
|
|
||||||
data: string;
|
|
||||||
_deleted?: boolean;
|
|
||||||
type: "leaf";
|
|
||||||
_rev?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntryVersionInfo {
|
|
||||||
_id: typeof VERSIONINFO_DOCID;
|
|
||||||
_rev?: string;
|
|
||||||
type: "versioninfo";
|
|
||||||
version: number;
|
|
||||||
_deleted?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntryMilestoneInfo {
|
|
||||||
_id: typeof MILSTONE_DOCID;
|
|
||||||
_rev?: string;
|
|
||||||
type: "milestoneinfo";
|
|
||||||
_deleted?: boolean;
|
|
||||||
created: number;
|
|
||||||
accepted_nodes: string[];
|
|
||||||
locked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntryNodeInfo {
|
|
||||||
_id: typeof NODEINFO_DOCID;
|
|
||||||
_rev?: string;
|
|
||||||
_deleted?: boolean;
|
|
||||||
type: "nodeinfo";
|
|
||||||
nodeid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EntryBody = Entry | NewEntry | PlainEntry;
|
|
||||||
export type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo;
|
|
||||||
|
|
||||||
export type diff_result_leaf = {
|
|
||||||
rev: string;
|
|
||||||
data: string;
|
|
||||||
ctime: number;
|
|
||||||
mtime: number;
|
|
||||||
};
|
|
||||||
export type dmp_result = Array<[number, string]>;
|
|
||||||
|
|
||||||
export type diff_result = {
|
|
||||||
left: diff_result_leaf;
|
|
||||||
right: diff_result_leaf;
|
|
||||||
diff: dmp_result;
|
|
||||||
};
|
|
||||||
export type diff_check_result = boolean | diff_result;
|
|
||||||
|
|
||||||
export type Credential = {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;
|
|
||||||
|
|
||||||
export type DatabaseConnectingStatus = "STARTED" | "NOT_CONNECTED" | "PAUSED" | "CONNECTED" | "COMPLETED" | "CLOSED" | "ERRORED";
|
|
||||||
|
|
||||||
export interface PluginList {
|
export interface PluginList {
|
||||||
[key: string]: PluginDataEntry[];
|
[key: string]: PluginDataEntry[];
|
||||||
}
|
}
|
||||||
@@ -229,5 +21,4 @@ export interface PluginList {
|
|||||||
export interface DevicePluginList {
|
export interface DevicePluginList {
|
||||||
[key: string]: PluginDataEntry;
|
[key: string]: PluginDataEntry;
|
||||||
}
|
}
|
||||||
|
export const PERIODIC_PLUGIN_SWEEP = 60;
|
||||||
export const FLAGMD_REDFLAG = "redflag.md";
|
|
||||||
|
|||||||
243
src/utils.ts
243
src/utils.ts
@@ -1,249 +1,14 @@
|
|||||||
import { normalizePath } from "obsidian";
|
import { normalizePath } from "obsidian";
|
||||||
import { Logger } from "./logger";
|
|
||||||
import { FLAGMD_REDFLAG, LOG_LEVEL } from "./types";
|
|
||||||
|
|
||||||
export function arrayBufferToBase64(buffer: ArrayBuffer): Promise<string> {
|
import { path2id_base, id2path_base } from "./lib/src/utils";
|
||||||
return new Promise((res) => {
|
|
||||||
const blob = new Blob([buffer], { type: "application/octet-binary" });
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function (evt) {
|
|
||||||
const dataurl = evt.target.result.toString();
|
|
||||||
res(dataurl.substr(dataurl.indexOf(",") + 1));
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function base64ToString(base64: string): string {
|
|
||||||
try {
|
|
||||||
const binary_string = window.atob(base64);
|
|
||||||
const len = binary_string.length;
|
|
||||||
const bytes = new Uint8Array(len);
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
bytes[i] = binary_string.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return new TextDecoder().decode(bytes);
|
|
||||||
} catch (ex) {
|
|
||||||
return base64;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
||||||
try {
|
|
||||||
const binary_string = window.atob(base64);
|
|
||||||
const len = binary_string.length;
|
|
||||||
const bytes = new Uint8Array(len);
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
bytes[i] = binary_string.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes.buffer;
|
|
||||||
} catch (ex) {
|
|
||||||
try {
|
|
||||||
return new Uint16Array(
|
|
||||||
[].map.call(base64, function (c: string) {
|
|
||||||
return c.charCodeAt(0);
|
|
||||||
})
|
|
||||||
).buffer;
|
|
||||||
} catch (ex2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const escapeStringToHTML = (str: string) => {
|
|
||||||
if (!str) return "";
|
|
||||||
return str.replace(/[<>&"'`]/g, (match) => {
|
|
||||||
const escape: any = {
|
|
||||||
"<": "<",
|
|
||||||
">": ">",
|
|
||||||
"&": "&",
|
|
||||||
'"': """,
|
|
||||||
"'": "'",
|
|
||||||
"`": "`",
|
|
||||||
};
|
|
||||||
return escape[match];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function resolveWithIgnoreKnownError<T>(p: Promise<T>, def: T): Promise<T> {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
p.then(res).catch((ex) => (ex.status && ex.status == 404 ? res(def) : rej(ex)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidPath(filename: string): boolean {
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
const regex = /[\u0000-\u001f]|[\\":?<>|*#]/g;
|
|
||||||
let x = filename.replace(regex, "_");
|
|
||||||
const win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi;
|
|
||||||
const sx = (x = x.replace(win, "/_"));
|
|
||||||
return sx == filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldBeIgnored(filename: string): boolean {
|
|
||||||
if (filename == FLAGMD_REDFLAG) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function versionNumberString2Number(version: string): number {
|
|
||||||
return version // "1.23.45"
|
|
||||||
.split(".") // 1 23 45
|
|
||||||
.reverse() // 45 23 1
|
|
||||||
.map((e, i) => ((e as any) / 1) * 1000 ** i) // 45 23000 1000000
|
|
||||||
.reduce((prev, current) => prev + current, 0); // 1023045
|
|
||||||
}
|
|
||||||
|
|
||||||
export const delay = (ms: number): Promise<void> => {
|
|
||||||
return new Promise((res) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
res();
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// For backward compatibility, using the path for determining id.
|
// For backward compatibility, using the path for determining id.
|
||||||
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
|
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||||
// The first slash will be deleted when the path is normalized.
|
// The first slash will be deleted when the path is normalized.
|
||||||
export function path2id(filename: string): string {
|
export function path2id(filename: string): string {
|
||||||
let x = normalizePath(filename);
|
const x = normalizePath(filename);
|
||||||
if (x.startsWith("_")) x = "/" + x;
|
return path2id_base(x);
|
||||||
return x;
|
|
||||||
}
|
}
|
||||||
export function id2path(filename: string): string {
|
export function id2path(filename: string): string {
|
||||||
return normalizePath(filename);
|
return id2path_base(normalizePath(filename));
|
||||||
}
|
|
||||||
|
|
||||||
const runningProcs: string[] = [];
|
|
||||||
const pendingProcs: { [key: string]: (() => Promise<void>)[] } = {};
|
|
||||||
function objectToKey(key: any): string {
|
|
||||||
if (typeof key === "string") return key;
|
|
||||||
const keys = Object.keys(key).sort((a, b) => a.localeCompare(b));
|
|
||||||
return keys.map((e) => e + objectToKey(key[e])).join(":");
|
|
||||||
}
|
|
||||||
export function getProcessingCounts() {
|
|
||||||
let count = 0;
|
|
||||||
for (const v in pendingProcs) {
|
|
||||||
count += pendingProcs[v].length;
|
|
||||||
}
|
|
||||||
count += runningProcs.length;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
let externalNotifier: () => void = () => {};
|
|
||||||
let notifyTimer: number = null;
|
|
||||||
export function setLockNotifier(fn: () => void) {
|
|
||||||
externalNotifier = fn;
|
|
||||||
}
|
|
||||||
function notifyLock() {
|
|
||||||
if (notifyTimer != null) {
|
|
||||||
window.clearTimeout(notifyTimer);
|
|
||||||
}
|
|
||||||
notifyTimer = window.setTimeout(() => {
|
|
||||||
externalNotifier();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
// Just run async/await as like transacion ISOLATION SERIALIZABLE
|
|
||||||
export function runWithLock<T>(key: unknown, ignoreWhenRunning: boolean, proc: () => Promise<T>): Promise<T> {
|
|
||||||
// Logger(`Lock:${key}:enter`, LOG_LEVEL.VERBOSE);
|
|
||||||
const lockKey = typeof key === "string" ? key : objectToKey(key);
|
|
||||||
const handleNextProcs = () => {
|
|
||||||
if (typeof pendingProcs[lockKey] === "undefined") {
|
|
||||||
//simply unlock
|
|
||||||
runningProcs.remove(lockKey);
|
|
||||||
notifyLock();
|
|
||||||
// Logger(`Lock:${lockKey}:released`, LOG_LEVEL.VERBOSE);
|
|
||||||
} else {
|
|
||||||
Logger(`Lock:${lockKey}:left ${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
|
||||||
let nextProc = null;
|
|
||||||
nextProc = pendingProcs[lockKey].shift();
|
|
||||||
notifyLock();
|
|
||||||
if (nextProc) {
|
|
||||||
// left some
|
|
||||||
nextProc()
|
|
||||||
.then()
|
|
||||||
.catch((err) => {
|
|
||||||
Logger(err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) {
|
|
||||||
delete pendingProcs[lockKey];
|
|
||||||
notifyLock();
|
|
||||||
}
|
|
||||||
queueMicrotask(() => {
|
|
||||||
handleNextProcs();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) {
|
|
||||||
delete pendingProcs[lockKey];
|
|
||||||
notifyLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (runningProcs.contains(lockKey)) {
|
|
||||||
if (ignoreWhenRunning) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeof pendingProcs[lockKey] === "undefined") {
|
|
||||||
pendingProcs[lockKey] = [];
|
|
||||||
}
|
|
||||||
let responderRes: (value: T | PromiseLike<T>) => void;
|
|
||||||
let responderRej: (reason?: unknown) => void;
|
|
||||||
const responder = new Promise<T>((res, rej) => {
|
|
||||||
responderRes = res;
|
|
||||||
responderRej = rej;
|
|
||||||
//wait for subproc resolved
|
|
||||||
});
|
|
||||||
const subproc = () =>
|
|
||||||
new Promise<void>((res, rej) => {
|
|
||||||
proc()
|
|
||||||
.then((v) => {
|
|
||||||
// Logger(`Lock:${key}:processed`, LOG_LEVEL.VERBOSE);
|
|
||||||
handleNextProcs();
|
|
||||||
responderRes(v);
|
|
||||||
res();
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
Logger(`Lock:${key}:rejected`, LOG_LEVEL.VERBOSE);
|
|
||||||
handleNextProcs();
|
|
||||||
rej(reason);
|
|
||||||
responderRej(reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
pendingProcs[lockKey].push(subproc);
|
|
||||||
notifyLock();
|
|
||||||
// Logger(`Lock:${lockKey}:queud:left${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE);
|
|
||||||
return responder;
|
|
||||||
} else {
|
|
||||||
runningProcs.push(lockKey);
|
|
||||||
notifyLock();
|
|
||||||
// Logger(`Lock:${lockKey}:aqquired`, LOG_LEVEL.VERBOSE);
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
proc()
|
|
||||||
.then((v) => {
|
|
||||||
handleNextProcs();
|
|
||||||
res(v);
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
handleNextProcs();
|
|
||||||
rej(reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPlainText(filename: string): boolean {
|
|
||||||
if (filename.endsWith(".md")) return true;
|
|
||||||
if (filename.endsWith(".txt")) return true;
|
|
||||||
if (filename.endsWith(".svg")) return true;
|
|
||||||
if (filename.endsWith(".html")) return true;
|
|
||||||
if (filename.endsWith(".csv")) return true;
|
|
||||||
if (filename.endsWith(".css")) return true;
|
|
||||||
if (filename.endsWith(".js")) return true;
|
|
||||||
if (filename.endsWith(".xml")) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Logger } from "./logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc } from "./types";
|
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc } from "./lib/src/types";
|
||||||
import { resolveWithIgnoreKnownError } from "./utils";
|
import { resolveWithIgnoreKnownError } from "./lib/src/utils";
|
||||||
import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
import { PouchDB } from "./pouchdb-browser";
|
||||||
|
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||||
|
|
||||||
export const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
export const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
||||||
if (uri.startsWith("https://")) return true;
|
if (uri.startsWith("https://")) return true;
|
||||||
@@ -12,13 +13,34 @@ let last_post_successed = false;
|
|||||||
export const getLastPostFailedBySize = () => {
|
export const getLastPostFailedBySize = () => {
|
||||||
return !last_post_successed;
|
return !last_post_successed;
|
||||||
};
|
};
|
||||||
export const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> => {
|
const fetchByAPI = async (request: RequestUrlParam): Promise<RequestUrlResponse> => {
|
||||||
|
const ret = await requestUrl(request);
|
||||||
|
if (ret.status - (ret.status % 100) !== 200) {
|
||||||
|
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
|
||||||
|
if (ret.json) {
|
||||||
|
er.message = ret.json.reason;
|
||||||
|
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
|
||||||
|
}
|
||||||
|
er.status = ret.status;
|
||||||
|
throw er;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }, disableRequestURI: boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> => {
|
||||||
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
||||||
|
let authHeader = "";
|
||||||
|
if (auth.username && auth.password) {
|
||||||
|
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
|
||||||
|
const encoded = window.btoa(utf8str);
|
||||||
|
authHeader = "Basic " + encoded;
|
||||||
|
} else {
|
||||||
|
authHeader = "";
|
||||||
|
}
|
||||||
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
|
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
|
||||||
adapter: "http",
|
adapter: "http",
|
||||||
auth,
|
auth,
|
||||||
fetch: async function (url: string | Request, opts: RequestInit) {
|
fetch: async function (url: string | Request, opts: RequestInit) {
|
||||||
let size_ok = true;
|
|
||||||
let size = "";
|
let size = "";
|
||||||
const localURL = url.toString().substring(uri.length);
|
const localURL = url.toString().substring(uri.length);
|
||||||
const method = opts.method ?? "GET";
|
const method = opts.method ?? "GET";
|
||||||
@@ -26,7 +48,6 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
const opts_length = opts.body.toString().length;
|
const opts_length = opts.body.toString().length;
|
||||||
if (opts_length > 1024 * 1024 * 10) {
|
if (opts_length > 1024 * 1024 * 10) {
|
||||||
// over 10MB
|
// over 10MB
|
||||||
size_ok = false;
|
|
||||||
if (uri.contains(".cloudantnosqldb.")) {
|
if (uri.contains(".cloudantnosqldb.")) {
|
||||||
last_post_successed = false;
|
last_post_successed = false;
|
||||||
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
||||||
@@ -35,6 +56,52 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
}
|
}
|
||||||
size = ` (${opts_length})`;
|
size = ` (${opts_length})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!disableRequestURI && typeof url == "string" && typeof (opts.body ?? "") == "string") {
|
||||||
|
const body = opts.body as string;
|
||||||
|
|
||||||
|
const transformedHeaders = { ...(opts.headers as Record<string, string>) };
|
||||||
|
if (authHeader != "") transformedHeaders["authorization"] = authHeader;
|
||||||
|
delete transformedHeaders["host"];
|
||||||
|
delete transformedHeaders["Host"];
|
||||||
|
delete transformedHeaders["content-length"];
|
||||||
|
delete transformedHeaders["Content-Length"];
|
||||||
|
const requestParam: RequestUrlParam = {
|
||||||
|
url: url as string,
|
||||||
|
method: opts.method,
|
||||||
|
body: body,
|
||||||
|
headers: transformedHeaders,
|
||||||
|
contentType: "application/json",
|
||||||
|
// contentType: opts.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetchByAPI(requestParam);
|
||||||
|
if (method == "POST" || method == "PUT") {
|
||||||
|
last_post_successed = r.status - (r.status % 100) == 200;
|
||||||
|
} else {
|
||||||
|
last_post_successed = true;
|
||||||
|
}
|
||||||
|
Logger(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL.VERBOSE);
|
||||||
|
|
||||||
|
return new Response(r.arrayBuffer, {
|
||||||
|
headers: r.headers,
|
||||||
|
status: r.status,
|
||||||
|
statusText: `${r.status}`,
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
||||||
|
// limit only in bulk_docs.
|
||||||
|
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||||
|
last_post_successed = false;
|
||||||
|
}
|
||||||
|
Logger(ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -old implementation
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const responce: Response = await fetch(url, opts);
|
const responce: Response = await fetch(url, opts);
|
||||||
if (method == "POST" || method == "PUT") {
|
if (method == "POST" || method == "PUT") {
|
||||||
@@ -46,7 +113,8 @@ export const connectRemoteCouchDB = async (uri: string, auth: { username: string
|
|||||||
return responce;
|
return responce;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
||||||
if (!size_ok && (method == "POST" || method == "PUT")) {
|
// limit only in bulk_docs.
|
||||||
|
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||||
last_post_successed = false;
|
last_post_successed = false;
|
||||||
}
|
}
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"types": ["svelte", "node"],
|
|
||||||
// "importsNotUsedAsValues": "error",
|
// "importsNotUsedAsValues": "error",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user