mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-24 21:18:47 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de5cdf507d | ||
|
|
83209f3923 | ||
|
|
b14ecdb205 | ||
|
|
21362adb5b | ||
|
|
f8c1474700 | ||
|
|
b35052a485 | ||
|
|
c367d35e09 | ||
|
|
2a5078cdbb | ||
|
|
8112a07210 | ||
|
|
c9daa1b47d | ||
|
|
73ac93e8c5 | ||
|
|
8d2b9eff37 | ||
|
|
0ee32a2147 | ||
|
|
ac3c78e198 | ||
|
|
0da1e3d9c8 | ||
|
|
8f021a3c93 | ||
|
|
6db0743096 | ||
|
|
0e300a0a6b | ||
|
|
9d0ffd1848 | ||
|
|
e7f4d8c9c2 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "::set-output name=tag::$(git describe --abbrev=0)"
|
||||
echo "::set-output name=tag::$(git describe --abbrev=0 --tags)"
|
||||
# Build the plugin
|
||||
- name: Build
|
||||
id: build
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ main.js
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
.vscode
|
||||
|
||||
@@ -43,7 +43,7 @@ Note: More information about alternative hosting methods needed! Currently, [usi
|
||||
### First device
|
||||
|
||||
1. Install the plugin on your device.
|
||||
2. Configure remote database infomation.
|
||||
2. Configure remote database information.
|
||||
1. Fill your server's information into the `Remote Database configuration` pane.
|
||||
2. Enabling `End to End Encryption` is recommended. After entering a passphrase, click `Apply`.
|
||||
3. Click `Test Database Connection` and make sure that the plugin says `Connected to (your-database-name)`.
|
||||
@@ -53,7 +53,7 @@ Note: More information about alternative hosting methods needed! Currently, [usi
|
||||
2. Or, set up the synchronization as you like. By default, none of the settings are enabled, meaning you would need to manually trigger the synchronization process.
|
||||
3. Additional configurations are also here. I recommend enabling `Use Trash for deleted files`, but you can also leave all configurations as-is.
|
||||
4. Configure miscellaneous features.
|
||||
1. Enabling `Show staus inside editor` shows status at the top-right corner of the editor while in editing mode. (Recommended)
|
||||
1. Enabling `Show status inside editor` shows status at the top-right corner of the editor while in editing mode. (Recommended)
|
||||
5. Go back to the editor. Wait for the initial scan to complete.
|
||||
6. When the status no longer changes and shows a ⏹️ for COMPLETED (No ⏳ and 🧩 icons), you are ready to synchronize with the server.
|
||||
7. Press the replicate icon on the Ribbon or run `Replicate now` from the command palette. This will send all your data to the server.
|
||||
@@ -115,7 +115,7 @@ If you have deleted or renamed files, please wait until ⏳ icon disappeared.
|
||||
- While synchronizing, files are compared by their modification time and the older ones will be overwritten by the newer ones. Then plugin checks for conflicts and if a merge is needed, a dialog will open.
|
||||
- Rarely, a file in the database could be corrupted. The plugin will not write to local storage when a file looks corrupted. If a local version of the file is on your device, the corruption could be fixed by editing the local file and synchronizing it. But if the file does not exist on any of your devices, then it can not be rescued. In this case you can delete these items from the settings dialog.
|
||||
- If your database looks corrupted, try "Drop History". Usually, It is the easiest way.
|
||||
- To stop the bootup sequence (eg. for fixing problems on databases), you can put a `redflag.md` file at the root of your vault.
|
||||
- To stop the boot up sequence (eg. for fixing problems on databases), you can put a `redflag.md` file at the root of your vault.
|
||||
- Q: Database is growing, how can I shrink it down?
|
||||
A: each of the docs is saved with their past 100 revisions for detecting and resolving conflicts. Picturing that one device has been offline for a while, and comes online again. The device has to compare its notes with the remotely saved ones. If there exists a historic revision in which the note used to be identical, it could be updated safely (like git fast-forward). Even if that is not in revision histories, we only have to check the differences after the revision that both devices commonly have. This is like git's conflict resolving method. So, We have to make the database again like an enlarged git repo if you want to solve the root of the problem.
|
||||
- And more technical Information are in the [Technical Information](docs/tech_info.md)
|
||||
|
||||
@@ -29,7 +29,7 @@ esbuild
|
||||
external: ["obsidian", "electron", ...builtins],
|
||||
format: "cjs",
|
||||
watch: !prod,
|
||||
target: "es2015",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.13.4",
|
||||
"version": "0.14.8",
|
||||
"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.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.13.3",
|
||||
"version": "0.14.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.13.3",
|
||||
"version": "0.14.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.13.4",
|
||||
"version": "0.14.8",
|
||||
"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",
|
||||
"type": "module",
|
||||
|
||||
1
pouchdb-browser-webpack/.gitignore
vendored
1
pouchdb-browser-webpack/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
@@ -1,4 +0,0 @@
|
||||
# PouchDB-browser
|
||||
|
||||
Just webpacked.
|
||||
(Rollup couldn't pack pouchdb-browser into browser bundle)
|
||||
File diff suppressed because one or more lines are too long
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "pouchdb-browser-webpack",
|
||||
"version": "1.0.0",
|
||||
"description": "pouchdb-browser webpack",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack --mode=production --node-env=production",
|
||||
"build:dev": "webpack --mode=development",
|
||||
"build:prod": "webpack --mode=production --node-env=production",
|
||||
"watch": "webpack --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pouchdb-browser": "^7.3.0",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"pouchdb-find": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "^5.58.1",
|
||||
"webpack-cli": "^4.9.0"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
// This module just webpacks pouchdb-browser
|
||||
// import * as PouchDB_src from "pouchdb-browser";
|
||||
const pouch = require("pouchdb-browser").default;
|
||||
const find = require("pouchdb-find").default;
|
||||
const transform = require("transform-pouch");
|
||||
const PouchDB = pouch.plugin(find).plugin(transform);
|
||||
|
||||
export { PouchDB };
|
||||
@@ -1,30 +0,0 @@
|
||||
// Generated using webpack-cli https://github.com/webpack/webpack-cli
|
||||
|
||||
const path = require("path");
|
||||
|
||||
const isProduction = process.env.NODE_ENV == "production";
|
||||
|
||||
const config = {
|
||||
entry: "./src/index.js",
|
||||
output: {
|
||||
filename: "pouchdb-browser.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
library: {
|
||||
type: "module",
|
||||
},
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true,
|
||||
},
|
||||
plugins: [],
|
||||
module: {},
|
||||
};
|
||||
|
||||
module.exports = () => {
|
||||
if (isProduction) {
|
||||
config.mode = "production";
|
||||
} else {
|
||||
config.mode = "development";
|
||||
}
|
||||
return config;
|
||||
};
|
||||
@@ -31,14 +31,25 @@ export class DocumentHistoryModal extends Modal {
|
||||
}
|
||||
async loadFile() {
|
||||
const db = this.plugin.localDatabase;
|
||||
const w = await db.localDatabase.get(path2id(this.file), { revs_info: true });
|
||||
this.revs_info = w._revs_info.filter((e) => e.status == "available");
|
||||
this.range.max = `${this.revs_info.length - 1}`;
|
||||
this.range.value = this.range.max;
|
||||
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
||||
await this.loadRevs();
|
||||
try {
|
||||
const w = await db.localDatabase.get(path2id(this.file), { revs_info: true });
|
||||
this.revs_info = w._revs_info.filter((e) => e.status == "available");
|
||||
this.range.max = `${this.revs_info.length - 1}`;
|
||||
this.range.value = this.range.max;
|
||||
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
||||
await this.loadRevs();
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
this.range.max = "0";
|
||||
this.range.value = "";
|
||||
this.range.disabled = true;
|
||||
this.showDiff
|
||||
this.contentView.setText(`History of this file was not recorded.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
async loadRevs() {
|
||||
if (this.revs_info.length == 0) return;
|
||||
const db = this.plugin.localDatabase;
|
||||
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
||||
const rev = this.revs_info[index];
|
||||
@@ -154,7 +165,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
const leaf = app.workspace.getLeaf(false);
|
||||
await leaf.openFile(targetFile);
|
||||
} else {
|
||||
Logger("The file cound not view on the editor", LOG_LEVEL.NOTICE)
|
||||
Logger("The file could not view on the editor", LOG_LEVEL.NOTICE)
|
||||
}
|
||||
}
|
||||
buttons.createEl("button", { text: "Back to this revision" }, (e) => {
|
||||
@@ -162,7 +173,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
e.addEventListener("click", async () => {
|
||||
const pathToWrite = this.file.startsWith("i:") ? this.file.substring("i:".length) : this.file;
|
||||
if (!isValidPath(pathToWrite)) {
|
||||
Logger("Path is not vaild to write content.", LOG_LEVEL.INFO);
|
||||
Logger("Path is not valid to write content.", LOG_LEVEL.INFO);
|
||||
}
|
||||
if (this.currentDoc?.datatype == "plain") {
|
||||
await this.app.vault.adapter.write(pathToWrite, this.currentDoc.data);
|
||||
|
||||
1385
src/LocalPouchDB.ts
1385
src/LocalPouchDB.ts
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import { EntryDoc, LOG_LEVEL, RemoteDBSettings } from "./lib/src/types";
|
||||
import { path2id, id2path } from "./utils";
|
||||
import { delay, runWithLock, versionNumberString2Number } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { checkSyncInfo, connectRemoteCouchDBWithSetting } from "./utils_couchdb";
|
||||
import { checkSyncInfo } from "./lib/src/utils_couchdb";
|
||||
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
async testConnection(): Promise<void> {
|
||||
const db = await connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.localDatabase.isMobile);
|
||||
const db = await this.plugin.localDatabase.connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.localDatabase.isMobile);
|
||||
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);
|
||||
return;
|
||||
@@ -49,7 +49,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='60' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔌</div></label>
|
||||
<label class='sls-setting-label'><input type='radio' name='disp' value='70' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🚑</div></label>
|
||||
`;
|
||||
const menutabs = w.querySelectorAll(".sls-setting-label");
|
||||
const menuTabs = w.querySelectorAll(".sls-setting-label");
|
||||
const changeDisplay = (screen: string) => {
|
||||
for (const k in screenElements) {
|
||||
if (k == screen) {
|
||||
@@ -59,11 +59,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}
|
||||
};
|
||||
menutabs.forEach((element) => {
|
||||
menuTabs.forEach((element) => {
|
||||
const e = element.querySelector(".sls-setting-tab");
|
||||
if (!e) return;
|
||||
e.addEventListener("change", (event) => {
|
||||
menutabs.forEach((element) => element.removeClass("selected"));
|
||||
menuTabs.forEach((element) => element.removeClass("selected"));
|
||||
changeDisplay((event.currentTarget as HTMLInputElement).value);
|
||||
element.addClass("selected");
|
||||
});
|
||||
@@ -115,12 +115,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
};
|
||||
const applyDisplayEnabled = () => {
|
||||
if (isAnySyncEnabled()) {
|
||||
dbsettings.forEach((e) => {
|
||||
dbSettings.forEach((e) => {
|
||||
e.setDisabled(true).setTooltip("Could not change this while any synchronization options are enabled.");
|
||||
});
|
||||
syncWarn.removeClass("sls-hidden");
|
||||
} else {
|
||||
dbsettings.forEach((e) => {
|
||||
dbSettings.forEach((e) => {
|
||||
e.setDisabled(false).setTooltip("");
|
||||
});
|
||||
syncWarn.addClass("sls-hidden");
|
||||
@@ -149,8 +149,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
};
|
||||
|
||||
const dbsettings: Setting[] = [];
|
||||
dbsettings.push(
|
||||
const dbSettings: Setting[] = [];
|
||||
dbSettings.push(
|
||||
new Setting(containerRemoteDatabaseEl).setName("URI").addText((text) =>
|
||||
text
|
||||
.setPlaceholder("https://........")
|
||||
@@ -201,11 +201,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => {
|
||||
this.plugin.settings.workingEncrypt = value;
|
||||
phasspharase.setDisabled(!value);
|
||||
passphrase.setDisabled(!value);
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
const phasspharase = new Setting(containerRemoteDatabaseEl)
|
||||
const passphrase = new Setting(containerRemoteDatabaseEl)
|
||||
.setName("Passphrase")
|
||||
.setDesc("Encrypting passphrase. If you change the passphrase of a existing database, overwriting the remote database is strongly recommended.")
|
||||
.addText((text) => {
|
||||
@@ -217,7 +217,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
});
|
||||
text.inputEl.setAttribute("type", "password");
|
||||
});
|
||||
phasspharase.setDisabled(!this.plugin.settings.workingEncrypt);
|
||||
passphrase.setDisabled(!this.plugin.settings.workingEncrypt);
|
||||
const checkWorkingPassphrase = async (): Promise<boolean> => {
|
||||
const settingForCheck: RemoteDBSettings = {
|
||||
...this.plugin.settings,
|
||||
@@ -225,7 +225,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
passphrase: this.plugin.settings.workingPassphrase,
|
||||
};
|
||||
console.dir(settingForCheck);
|
||||
const db = await connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.localDatabase.isMobile);
|
||||
const db = await this.plugin.localDatabase.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.localDatabase.isMobile);
|
||||
if (typeof db === "string") {
|
||||
Logger("Could not connect to the database.", LOG_LEVEL.NOTICE);
|
||||
return false;
|
||||
@@ -417,7 +417,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
const res = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, undefined, key, value);
|
||||
console.dir(res);
|
||||
if (res.status == 200) {
|
||||
Logger(`${title} successfly updated`, LOG_LEVEL.NOTICE);
|
||||
Logger(`${title} successfully updated`, LOG_LEVEL.NOTICE);
|
||||
checkResultDiv.removeChild(x);
|
||||
checkConfig();
|
||||
} else {
|
||||
@@ -469,6 +469,22 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
} else {
|
||||
addResult("✔ httpd.enable_cors is ok.");
|
||||
}
|
||||
// If the server is not cloudant, configure request size
|
||||
if (!this.plugin.settings.couchDB_URI.contains(".cloudantnosqldb.")) {
|
||||
// REQUEST SIZE
|
||||
if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) {
|
||||
addResult("❗ chttpd.max_http_request_size is low)");
|
||||
addConfigFixButton("Set chttpd.max_http_request_size", "chttpd/max_http_request_size", "4294967296");
|
||||
} else {
|
||||
addResult("✔ chttpd.max_http_request_size is ok.");
|
||||
}
|
||||
if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) {
|
||||
addResult("❗ couchdb.max_document_size is low)");
|
||||
addConfigFixButton("Set couchdb.max_document_size", "couchdb/max_document_size", "50000000");
|
||||
} else {
|
||||
addResult("✔ couchdb.max_document_size is ok.");
|
||||
}
|
||||
}
|
||||
// CORS check
|
||||
// checking connectivity for mobile
|
||||
if (responseConfig?.cors?.credentials != "true") {
|
||||
@@ -515,10 +531,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
addResult("✔ CORS origin OK");
|
||||
}
|
||||
}
|
||||
addResult("--Done--", ["ob-btn-config-haed"]);
|
||||
addResult("--Done--", ["ob-btn-config-head"]);
|
||||
addResult("If you have some trouble with Connection-check even though all Config-check has been passed, Please check your reverse proxy's configuration.", ["ob-btn-config-info"]);
|
||||
} catch (ex) {
|
||||
Logger(`Checking configration failed`);
|
||||
Logger(`Checking configuration failed`);
|
||||
Logger(ex);
|
||||
}
|
||||
};
|
||||
@@ -583,43 +599,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
})
|
||||
)
|
||||
|
||||
containerLocalDatabaseEl.createEl("div", {
|
||||
text: sanitizeHTMLToDom(`Advanced settings<br>
|
||||
Configuration of how LiveSync makes chunks from the file.`),
|
||||
});
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("Minimum chunk size")
|
||||
.setDesc("(letters), minimum chunk size.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.minimumChunkSize + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 10 || v > 1000) {
|
||||
v = 10;
|
||||
}
|
||||
this.plugin.settings.minimumChunkSize = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("LongLine Threshold")
|
||||
.setDesc("(letters), If the line is longer than this, make the line to chunk")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.longLineThreshold + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 10 || v > 1000) {
|
||||
v = 10;
|
||||
}
|
||||
this.plugin.settings.longLineThreshold = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
let newDatabaseName = this.plugin.settings.additionalSuffixOfDatabaseName + "";
|
||||
new Setting(containerLocalDatabaseEl)
|
||||
.setName("Database suffix")
|
||||
@@ -652,7 +632,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
new Setting(containerGeneralSettingsEl)
|
||||
.setName("Do not show low-priority Log")
|
||||
.setDesc("Reduce log infomations")
|
||||
.setDesc("Reduce log information")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.lessInformationInLog).onChange(async (value) => {
|
||||
this.plugin.settings.lessInformationInLog = value;
|
||||
@@ -661,7 +641,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
);
|
||||
new Setting(containerGeneralSettingsEl)
|
||||
.setName("Verbose Log")
|
||||
.setDesc("Show verbose log ")
|
||||
.setDesc("Show verbose log")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.showVerboseLog).onChange(async (value) => {
|
||||
this.plugin.settings.showVerboseLog = value;
|
||||
@@ -810,15 +790,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
// new Setting(containerSyncSettingEl)
|
||||
// .setName("Skip old files on sync")
|
||||
// .setDesc("Skip old incoming if incoming changes older than storage.")
|
||||
// .addToggle((toggle) =>
|
||||
// toggle.setValue(this.plugin.settings.skipOlderFilesOnSync).onChange(async (value) => {
|
||||
// this.plugin.settings.skipOlderFilesOnSync = value;
|
||||
// await this.plugin.saveSettings();
|
||||
// })
|
||||
// );
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Check conflict only on opened files")
|
||||
.setDesc("Do not check conflict for replication")
|
||||
@@ -829,9 +800,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
containerSyncSettingEl.createEl("h3", {
|
||||
text: sanitizeHTMLToDom(`Experimental`),
|
||||
});
|
||||
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Sync hidden files")
|
||||
.addToggle((toggle) =>
|
||||
@@ -926,6 +895,86 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
})
|
||||
)
|
||||
|
||||
containerSyncSettingEl.createEl("h3", {
|
||||
text: sanitizeHTMLToDom(`Experimental`),
|
||||
});
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Regular expression to ignore files")
|
||||
.setDesc("If this is set, any changes to local and remote files that match this will be skipped.")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.syncIgnoreRegEx)
|
||||
.setPlaceholder("\\.pdf$")
|
||||
.onChange(async (value) => {
|
||||
let isValidRegExp = false;
|
||||
try {
|
||||
new RegExp(value);
|
||||
isValidRegExp = true;
|
||||
} catch (_) {
|
||||
// NO OP.
|
||||
}
|
||||
if (isValidRegExp || value.trim() == "") {
|
||||
this.plugin.settings.syncIgnoreRegEx = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
return text;
|
||||
}
|
||||
);
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Regular expression for restricting synchronization targets")
|
||||
.setDesc("If this is set, changes to local and remote files that only match this will be processed.")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.syncOnlyRegEx)
|
||||
.setPlaceholder("\\.md$|\\.txt")
|
||||
.onChange(async (value) => {
|
||||
let isValidRegExp = false;
|
||||
try {
|
||||
new RegExp(value);
|
||||
isValidRegExp = true;
|
||||
} catch (_) {
|
||||
// NO OP.
|
||||
}
|
||||
if (isValidRegExp || value.trim() == "") {
|
||||
this.plugin.settings.syncOnlyRegEx = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
return text;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Chunk size")
|
||||
.setDesc("Customize chunk size for binary files (0.1MBytes). This cannot be increased when using IBM Cloudant.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.customChunkSize + "")
|
||||
.onChange(async (value) => {
|
||||
let v = Number(value);
|
||||
if (isNaN(v) || v < 100) {
|
||||
v = 100;
|
||||
}
|
||||
this.plugin.settings.customChunkSize = v;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Read chunks online.")
|
||||
.setDesc("If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.")
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.readChunksOnline)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.readChunksOnline = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
return toggle;
|
||||
}
|
||||
);
|
||||
containerSyncSettingEl.createEl("h3", {
|
||||
text: sanitizeHTMLToDom(`Advanced settings`),
|
||||
});
|
||||
@@ -1064,7 +1113,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
c.addClass("op-warn");
|
||||
}
|
||||
}
|
||||
const hatchWarn = containerHatchEl.createEl("div", { text: `To stop the bootup sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
|
||||
const hatchWarn = containerHatchEl.createEl("div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
|
||||
hatchWarn.addClass("op-warn-info");
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
@@ -1179,7 +1228,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Drop old encrypted database")
|
||||
.setDesc("WARNING: Please use this button only when you have failed on converting old-style localdatabase at v0.10.0.")
|
||||
.setDesc("WARNING: Please use this button only when you have failed on converting old-style local database at v0.10.0.")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Drop")
|
||||
@@ -1193,7 +1242,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
addScreenElement("50", containerHatchEl);
|
||||
// With great respect, thank you TfTHacker!
|
||||
// refered: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
||||
// Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
||||
const containerPluginSettings = containerEl.createDiv();
|
||||
containerPluginSettings.createEl("h3", { text: "Plugins and settings (beta)" });
|
||||
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: a49a096a6a...57db68352b
365
src/main.ts
365
src/main.ts
@@ -17,8 +17,8 @@ import {
|
||||
setNoticeClass,
|
||||
NewNotice,
|
||||
getLocks,
|
||||
Parallels,
|
||||
WrappedNotice,
|
||||
Semaphore,
|
||||
} from "./lib/src/utils";
|
||||
import { Logger, setLogger } from "./lib/src/logger";
|
||||
import { LocalPouchDB } from "./LocalPouchDB";
|
||||
@@ -29,7 +29,7 @@ import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
|
||||
|
||||
|
||||
import { clearAllPeriodic, clearAllTriggers, clearTrigger, disposeMemoObject, id2path, memoIfNotExist, memoObject, path2id, retriveMemoObject, setTrigger } from "./utils";
|
||||
import { clearAllPeriodic, clearAllTriggers, clearTrigger, disposeMemoObject, id2path, memoIfNotExist, memoObject, path2id, retrieveMemoObject, setTrigger } from "./utils";
|
||||
import { decrypt, encrypt } from "./lib/src/e2ee_v2";
|
||||
|
||||
const isDebug = false;
|
||||
@@ -48,7 +48,7 @@ const ICHeaderLength = ICHeader.length;
|
||||
* @param str ID
|
||||
* @returns
|
||||
*/
|
||||
function isInteralChunk(str: string): boolean {
|
||||
function isInternalChunk(str: string): boolean {
|
||||
return str.startsWith(ICHeader);
|
||||
}
|
||||
function id2filenameInternalChunk(str: string): string {
|
||||
@@ -185,7 +185,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const doc = row.doc;
|
||||
nextKey = `${row.id}\u{10ffff}`;
|
||||
if (!("_conflicts" in doc)) continue;
|
||||
if (isInteralChunk(row.id)) continue;
|
||||
if (isInternalChunk(row.id)) continue;
|
||||
if (doc._deleted) continue;
|
||||
if ("deleted" in doc && doc.deleted) continue;
|
||||
if (doc.type == "newnote" || doc.type == "plain") {
|
||||
@@ -206,7 +206,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
const target = await askSelectString(this.app, "File to view History", notesList);
|
||||
if (target) {
|
||||
if (isInteralChunk(target)) {
|
||||
if (isInternalChunk(target)) {
|
||||
//NOP
|
||||
} else {
|
||||
await this.showIfConflicted(this.app.vault.getAbstractFileByPath(target) as TFile);
|
||||
@@ -224,8 +224,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
|
||||
Logger(`Self-hosted LiveSync v${manifestVersion} ${packageVersion} `);
|
||||
const lsname = "obsidian-live-sync-ver" + this.getVaultName();
|
||||
const last_version = localStorage.getItem(lsname);
|
||||
const lsKey = "obsidian-live-sync-ver" + this.getVaultName();
|
||||
const last_version = localStorage.getItem(lsKey);
|
||||
await this.loadSettings();
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
if (lastVersion > this.settings.lastReadUpdates) {
|
||||
@@ -245,7 +245,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.settings.versionUpFlash = "Self-hosted LiveSync has been upgraded and some behaviors have changed incompatibly. All automatic synchronization is now disabled temporary. Ensure that other devices are also upgraded, and enable synchronization again.";
|
||||
this.saveSettings();
|
||||
}
|
||||
localStorage.setItem(lsname, `${VER}`);
|
||||
localStorage.setItem(lsKey, `${VER}`);
|
||||
await this.openDatabase();
|
||||
|
||||
addIcon(
|
||||
@@ -286,7 +286,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||
this.watchVaultRename = this.watchVaultRename.bind(this);
|
||||
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false);
|
||||
this.watchWindowVisiblity = debounce(this.watchWindowVisiblity.bind(this), 1000, false);
|
||||
this.watchWindowVisibility = debounce(this.watchWindowVisibility.bind(this), 1000, false);
|
||||
this.watchOnline = debounce(this.watchOnline.bind(this), 500, false);
|
||||
|
||||
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
||||
|
||||
@@ -320,8 +321,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (this.settings.suspendFileWatching) {
|
||||
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE);
|
||||
}
|
||||
const isInitalized = await this.initializeDatabase();
|
||||
if (!isInitalized) {
|
||||
const isInitialized = await this.initializeDatabase();
|
||||
if (!isInitialized) {
|
||||
//TODO:stop all sync.
|
||||
return false;
|
||||
}
|
||||
@@ -361,19 +362,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
const config = decodeURIComponent(setupURI.substring(configURIBase.length));
|
||||
console.dir(config)
|
||||
await setupwizard(config);
|
||||
await setupWizard(config);
|
||||
},
|
||||
});
|
||||
const setupwizard = async (confString: string) => {
|
||||
const setupWizard = async (confString: string) => {
|
||||
try {
|
||||
const oldConf = JSON.parse(JSON.stringify(this.settings));
|
||||
const encryptingPassphrase = await askString(this.app, "Passphrase", "Passphrase for your settings", "");
|
||||
if (encryptingPassphrase === false) return;
|
||||
const newconf = await JSON.parse(await decrypt(confString, encryptingPassphrase));
|
||||
if (newconf) {
|
||||
const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase));
|
||||
if (newConf) {
|
||||
const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?");
|
||||
if (result == "yes") {
|
||||
const newSettingW = Object.assign({}, this.settings, newconf);
|
||||
const newSettingW = Object.assign({}, this.settings, newConf);
|
||||
// stopping once.
|
||||
this.localDatabase.closeReplication();
|
||||
this.settings.suspendFileWatching = true;
|
||||
@@ -401,6 +402,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
let initDB;
|
||||
this.settings = newSettingW;
|
||||
await this.saveSettings();
|
||||
if (keepLocalDB == "no") {
|
||||
this.resetLocalOldDatabase();
|
||||
@@ -437,7 +439,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
};
|
||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||
await setupwizard(conf.settings);
|
||||
await setupWizard(conf.settings);
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-replicate",
|
||||
@@ -448,7 +450,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-dump",
|
||||
name: "Dump informations of this doc ",
|
||||
name: "Dump information of this doc ",
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
|
||||
},
|
||||
@@ -504,6 +506,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.showHistory(view.file);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-scan-files",
|
||||
name: "Scan storage and database again",
|
||||
callback: async () => {
|
||||
await this.syncAllFiles(true)
|
||||
}
|
||||
})
|
||||
|
||||
this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000);
|
||||
this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000);
|
||||
@@ -534,14 +543,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-conflictcheck",
|
||||
name: "Pick a file to resolive conflict",
|
||||
name: "Pick a file to resolve conflict",
|
||||
callback: () => {
|
||||
this.pickFileForResolve();
|
||||
},
|
||||
})
|
||||
this.addCommand({
|
||||
id: "livesync-runbatch",
|
||||
name: "Run pending batch processes",
|
||||
name: "Run pended batch processes",
|
||||
callback: async () => {
|
||||
await this.applyBatchChange();
|
||||
},
|
||||
@@ -585,7 +594,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
clearAllPeriodic();
|
||||
clearAllTriggers();
|
||||
window.removeEventListener("visibilitychange", this.watchWindowVisiblity);
|
||||
window.removeEventListener("visibilitychange", this.watchWindowVisibility);
|
||||
window.removeEventListener("online", this.watchOnline)
|
||||
Logger("unloading plugin");
|
||||
}
|
||||
|
||||
@@ -620,15 +630,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
// So, use history is always enabled.
|
||||
this.settings.useHistory = true;
|
||||
|
||||
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
||||
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
||||
if (this.settings.deviceAndVaultName != "") {
|
||||
if (!localStorage.getItem(lsname)) {
|
||||
if (!localStorage.getItem(lsKey)) {
|
||||
this.deviceAndVaultName = this.settings.deviceAndVaultName;
|
||||
localStorage.setItem(lsname, this.deviceAndVaultName);
|
||||
localStorage.setItem(lsKey, this.deviceAndVaultName);
|
||||
this.settings.deviceAndVaultName = "";
|
||||
}
|
||||
}
|
||||
this.deviceAndVaultName = localStorage.getItem(lsname) || "";
|
||||
this.deviceAndVaultName = localStorage.getItem(lsKey) || "";
|
||||
}
|
||||
|
||||
triggerRealizeSettingSyncMode() {
|
||||
@@ -636,9 +646,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
||||
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
||||
|
||||
localStorage.setItem(lsname, this.deviceAndVaultName || "");
|
||||
localStorage.setItem(lsKey, this.deviceAndVaultName || "");
|
||||
await this.saveData(this.settings);
|
||||
this.localDatabase.settings = this.settings;
|
||||
this.triggerRealizeSettingSyncMode();
|
||||
@@ -666,14 +676,26 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
||||
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
|
||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
|
||||
window.addEventListener("visibilitychange", this.watchWindowVisibility);
|
||||
window.addEventListener("online", this.watchOnline);
|
||||
}
|
||||
|
||||
watchWindowVisiblity() {
|
||||
this.watchWindowVisiblityAsync();
|
||||
|
||||
watchOnline() {
|
||||
this.watchOnlineAsync();
|
||||
}
|
||||
async watchOnlineAsync() {
|
||||
// If some files were failed to retrieve, scan files again.
|
||||
if (navigator.onLine && this.localDatabase.needScanning) {
|
||||
this.localDatabase.needScanning = false;
|
||||
await this.syncAllFiles();
|
||||
}
|
||||
}
|
||||
watchWindowVisibility() {
|
||||
this.watchWindowVisibilityAsync();
|
||||
}
|
||||
|
||||
async watchWindowVisiblityAsync() {
|
||||
async watchWindowVisibilityAsync() {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
// if (this.suspended) return;
|
||||
const isHidden = document.hidden;
|
||||
@@ -718,6 +740,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
watchVaultCreate(file: TFile, ...args: any[]) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (recentlyTouched(file)) {
|
||||
return;
|
||||
@@ -726,6 +749,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
watchVaultChange(file: TAbstractFile, ...args: any[]) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
@@ -734,7 +758,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
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) {
|
||||
~(async () => {
|
||||
const meta = await this.localDatabase.getDBEntryMeta(file.path);
|
||||
@@ -760,27 +784,25 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return await runWithLock("batchSave", false, async () => {
|
||||
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
||||
this.batchFileChange = [];
|
||||
const limit = 3;
|
||||
const p = Parallels();
|
||||
const semaphore = Semaphore(3);
|
||||
|
||||
for (const e of batchItems) {
|
||||
const w = (async () => {
|
||||
try {
|
||||
const f = this.app.vault.getAbstractFileByPath(normalizePath(e));
|
||||
if (f && f instanceof TFile) {
|
||||
await this.updateIntoDB(f);
|
||||
Logger(`Batch save:${e}`);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
const batchProcesses = batchItems.map(e => (async (e) => {
|
||||
const releaser = await semaphore.acquire(1, "batch");
|
||||
try {
|
||||
const f = this.app.vault.getAbstractFileByPath(normalizePath(e));
|
||||
if (f && f instanceof TFile) {
|
||||
await this.updateIntoDB(f);
|
||||
Logger(`Batch save:${e}`);
|
||||
}
|
||||
})();
|
||||
p.add(w);
|
||||
await p.wait(limit)
|
||||
}
|
||||
this.refreshStatusText();
|
||||
await p.all();
|
||||
} catch (ex) {
|
||||
Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
} finally {
|
||||
releaser();
|
||||
}
|
||||
})(e))
|
||||
await Promise.all(batchProcesses);
|
||||
|
||||
this.refreshStatusText();
|
||||
return;
|
||||
});
|
||||
@@ -799,6 +821,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
watchVaultDelete(file: TAbstractFile) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
// When save is delayed, it should be cancelled.
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
@@ -830,6 +853,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
watchVaultRename(file: TAbstractFile, oldFile: any) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
this.watchVaultRenameAsync(file, oldFile).then(() => { });
|
||||
}
|
||||
@@ -899,32 +923,32 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL.VERBOSE) {
|
||||
return;
|
||||
}
|
||||
const valutName = this.getVaultName();
|
||||
const vaultName = this.getVaultName();
|
||||
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;
|
||||
const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
||||
const newMessage = timestamp + "->" + messageContent;
|
||||
|
||||
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
|
||||
console.log(valutName + ":" + newmessage);
|
||||
this.setStatusBarText(null, messagecontent.substring(0, 30));
|
||||
this.logMessage = [].concat(this.logMessage).concat([newMessage]).slice(-100);
|
||||
console.log(vaultName + ":" + newMessage);
|
||||
this.setStatusBarText(null, messageContent.substring(0, 30));
|
||||
// if (message instanceof Error) {
|
||||
// console.trace(message);
|
||||
// }
|
||||
|
||||
if (level >= LOG_LEVEL.NOTICE) {
|
||||
if (!key) key = messagecontent;
|
||||
if (!key) key = messageContent;
|
||||
if (key in this.notifies) {
|
||||
// @ts-ignore
|
||||
const isShown = this.notifies[key].notice.noticeEl?.isShown()
|
||||
if (!isShown) {
|
||||
this.notifies[key].notice = new Notice(messagecontent, 0);
|
||||
this.notifies[key].notice = new Notice(messageContent, 0);
|
||||
}
|
||||
clearTimeout(this.notifies[key].timer);
|
||||
if (key == messagecontent) {
|
||||
if (key == messageContent) {
|
||||
this.notifies[key].count++;
|
||||
this.notifies[key].notice.setMessage(`(${this.notifies[key].count}):${messagecontent}`);
|
||||
this.notifies[key].notice.setMessage(`(${this.notifies[key].count}):${messageContent}`);
|
||||
} else {
|
||||
this.notifies[key].notice.setMessage(`${messagecontent}`);
|
||||
this.notifies[key].notice.setMessage(`${messageContent}`);
|
||||
}
|
||||
|
||||
this.notifies[key].timer = setTimeout(() => {
|
||||
@@ -937,7 +961,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}, 5000);
|
||||
} else {
|
||||
const notify = new Notice(messagecontent, 0);
|
||||
const notify = new Notice(messageContent, 0);
|
||||
this.notifies[key] = {
|
||||
count: 0,
|
||||
notice: notify,
|
||||
@@ -951,8 +975,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (this.addLogHook != null) this.addLogHook();
|
||||
}
|
||||
|
||||
async ensureDirectory(fullpath: string) {
|
||||
const pathElements = fullpath.split("/");
|
||||
async ensureDirectory(fullPath: string) {
|
||||
const pathElements = fullPath.split("/");
|
||||
pathElements.pop();
|
||||
let c = "";
|
||||
for (const v of pathElements) {
|
||||
@@ -962,7 +986,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
} catch (ex) {
|
||||
// basically skip exceptions.
|
||||
if (ex.message && ex.message == "Folder already exists.") {
|
||||
// especialy this message is.
|
||||
// especially this message is.
|
||||
} else {
|
||||
Logger("Folder Create Error");
|
||||
Logger(ex);
|
||||
@@ -977,6 +1001,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (shouldBeIgnored(pathSrc)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isTargetFile(pathSrc)) return;
|
||||
|
||||
const doc = await this.localDatabase.getDBEntry(pathSrc, { rev: docEntry._rev });
|
||||
if (doc === false) return;
|
||||
const msg = `DB -> STORAGE (create${force ? ",force" : ""},${doc.datatype}) `;
|
||||
@@ -990,14 +1016,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
await this.ensureDirectory(path);
|
||||
try {
|
||||
const newfile = await this.app.vault.createBinary(normalizePath(path), bin, {
|
||||
const newFile = await this.app.vault.createBinary(normalizePath(path), bin, {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != newfile.path);
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
Logger(msg + path);
|
||||
touch(newfile);
|
||||
this.app.vault.trigger("create", newfile);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
} catch (ex) {
|
||||
Logger(msg + "ERROR, Could not write: " + path, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
@@ -1010,14 +1036,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
await this.ensureDirectory(path);
|
||||
try {
|
||||
const newfile = await this.app.vault.create(normalizePath(path), doc.data, {
|
||||
const newFile = await this.app.vault.create(normalizePath(path), doc.data, {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != newfile.path);
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
Logger(msg + path);
|
||||
touch(newfile);
|
||||
this.app.vault.trigger("create", newfile);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
} catch (ex) {
|
||||
Logger(msg + "ERROR, Could not parse: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
@@ -1028,6 +1054,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async deleteVaultItem(file: TFile | TFolder) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
const dir = file.parent;
|
||||
if (this.settings.trashInsteadDelete) {
|
||||
await this.app.vault.trash(file, false);
|
||||
@@ -1049,6 +1076,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (shouldBeIgnored(pathSrc)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isTargetFile(pathSrc)) return;
|
||||
if (docEntry._deleted || docEntry.deleted) {
|
||||
// This occurs not only when files are deleted, but also when conflicts are resolved.
|
||||
// We have to check no other revisions are left.
|
||||
@@ -1137,7 +1165,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await runWithLock("dbchanged", false, async () => {
|
||||
const w = [...this.queuedEntries];
|
||||
this.queuedEntries = [];
|
||||
Logger(`Applyng ${w.length} files`);
|
||||
Logger(`Applying ${w.length} files`);
|
||||
for (const entry of w) {
|
||||
Logger(`Applying ${entry._id} (${entry._rev}) change...`, LOG_LEVEL.VERBOSE);
|
||||
await this.handleDBChangedAsync(entry);
|
||||
@@ -1183,12 +1211,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
saveQueuedFiles() {
|
||||
const saveData = JSON.stringify(this.queuedFiles.filter((e) => !e.done).map((e) => e.entry._id));
|
||||
const lsname = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
localStorage.setItem(lsname, saveData);
|
||||
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
localStorage.setItem(lsKey, saveData);
|
||||
}
|
||||
async loadQueuedFiles() {
|
||||
const lsname = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
const ids = JSON.parse(localStorage.getItem(lsname) || "[]") as string[];
|
||||
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
const ids = JSON.parse(localStorage.getItem(lsKey) || "[]") as string[];
|
||||
const ret = await this.localDatabase.localDatabase.allDocs({ keys: ids, include_docs: true });
|
||||
for (const doc of ret.rows) {
|
||||
if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) {
|
||||
@@ -1220,7 +1248,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const now = new Date().getTime();
|
||||
if (queue.missingChildren.length == 0) {
|
||||
queue.done = true;
|
||||
if (isInteralChunk(queue.entry._id)) {
|
||||
if (isInternalChunk(queue.entry._id)) {
|
||||
//system file
|
||||
const filename = id2path(id2filenameInternalChunk(queue.entry._id));
|
||||
// await this.syncInternalFilesAndDatabase("pull", false, false, [filename])
|
||||
@@ -1260,8 +1288,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (isNewFileCompleted) this.procQueuedFiles();
|
||||
}
|
||||
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
|
||||
if (!this.isTargetFile(id2path(doc._id))) return;
|
||||
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||
if ((!isInteralChunk(doc._id)) && skipOldFile) {
|
||||
if ((!isInternalChunk(doc._id)) && skipOldFile) {
|
||||
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
|
||||
|
||||
if (info && info instanceof TFile) {
|
||||
@@ -1280,9 +1309,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
missingChildren: [] as string[],
|
||||
timeout: now + this.chunkWaitTimeout,
|
||||
};
|
||||
if ("children" in doc) {
|
||||
// If `Read chunks online` is enabled, retrieve chunks from the remote CouchDB directly.
|
||||
if ((!this.settings.readChunksOnline) && "children" in doc) {
|
||||
const c = await this.localDatabase.localDatabase.allDocs({ keys: doc.children, include_docs: false });
|
||||
const missing = c.rows.filter((e) => "error" in e).map((e) => e.key);
|
||||
// fetch from remote
|
||||
if (missing.length > 0) Logger(`${doc._id}(${doc._rev}) Queued (waiting ${missing.length} items)`, LOG_LEVEL.VERBOSE);
|
||||
newQueue.missingChildren = missing;
|
||||
this.queuedFiles.push(newQueue);
|
||||
@@ -1463,9 +1494,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const pieces = queue.map((e) => e[1].missingChildren).reduce((prev, cur) => prev + cur.length, 0);
|
||||
queued = ` 🧩 ${queuedCount} (${pieces})`;
|
||||
}
|
||||
const procs = getProcessingCounts();
|
||||
const procsDisp = procs == 0 ? "" : ` ⏳${procs}`;
|
||||
const message = `Sync: ${w} ↑${sent} ↓${arrived}${waiting}${procsDisp}${queued}`;
|
||||
const processes = getProcessingCounts();
|
||||
const processesDisp = processes == 0 ? "" : ` ⏳${processes}`;
|
||||
const message = `Sync: ${w} ↑${sent} ↓${arrived}${waiting}${processesDisp}${queued}`;
|
||||
const locks = getLocks();
|
||||
const pendingTask = locks.pending.length
|
||||
? "\nPending: " +
|
||||
@@ -1561,10 +1592,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger("Initializing", LOG_LEVEL.NOTICE, "syncAll");
|
||||
}
|
||||
|
||||
const filesStorage = this.app.vault.getFiles();
|
||||
const filesStorage = this.app.vault.getFiles().filter(e => this.isTargetFile(e));
|
||||
const filesStorageName = filesStorage.map((e) => e.path);
|
||||
const wf = await this.localDatabase.localDatabase.allDocs();
|
||||
const filesDatabase = wf.rows.filter((e) => !isChunk(e.id) && !isPluginChunk(e.id) && e.id != "obsydian_livesync_version").filter(e => isValidPath(e.id)).map((e) => id2path(e.id));
|
||||
const filesDatabase = wf.rows.filter((e) => !isChunk(e.id) && !isPluginChunk(e.id) && e.id != "obsydian_livesync_version").filter(e => isValidPath(e.id)).map((e) => id2path(e.id)).filter(e => this.isTargetFile(e));
|
||||
const isInitialized = await (this.localDatabase.kvDB.get<boolean>("initialized")) || false;
|
||||
// Make chunk bigger if it is the initial scan. There must be non-active docs.
|
||||
if (filesDatabase.length == 0 && !isInitialized) {
|
||||
@@ -1581,23 +1612,22 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
Logger("Updating database by new files");
|
||||
this.setStatusBarText(`UPDATE DATABASE`);
|
||||
|
||||
const runAll = async<T>(procedurename: string, objects: T[], callback: (arg: T) => Promise<void>) => {
|
||||
const runAll = async<T>(procedureName: string, objects: T[], callback: (arg: T) => Promise<void>) => {
|
||||
const count = objects.length;
|
||||
Logger(procedurename);
|
||||
Logger(procedureName);
|
||||
let i = 0;
|
||||
// let lastTicks = performance.now() + 2000;
|
||||
// let workProcs = 0;
|
||||
const p = Parallels();
|
||||
const limit = 10;
|
||||
const semaphore = Semaphore(10);
|
||||
|
||||
Logger(`${procedurename} exec.`);
|
||||
for (const v of objects) {
|
||||
// workProcs++;
|
||||
if (!this.localDatabase.isReady) throw Error("Database is not ready!");
|
||||
p.add(callback(v).then(() => {
|
||||
Logger(`${procedureName} exec.`);
|
||||
if (!this.localDatabase.isReady) throw Error("Database is not ready!");
|
||||
const processes = objects.map(e => (async (v) => {
|
||||
const releaser = await semaphore.acquire(1, procedureName);
|
||||
|
||||
try {
|
||||
await callback(v);
|
||||
i++;
|
||||
if (i % 100 == 0) {
|
||||
const notify = `${procedurename} : ${i}/${count}`;
|
||||
if (i % 50 == 0) {
|
||||
const notify = `${procedureName} : ${i}/${count}`;
|
||||
if (showingNotice) {
|
||||
Logger(notify, LOG_LEVEL.NOTICE, "syncAll");
|
||||
} else {
|
||||
@@ -1605,17 +1635,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
this.setStatusBarText(notify);
|
||||
}
|
||||
}).catch(ex => {
|
||||
Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE);
|
||||
} catch (ex) {
|
||||
Logger(`Error while ${procedureName}`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex);
|
||||
}).finally(() => {
|
||||
// workProcs--;
|
||||
})
|
||||
);
|
||||
await p.wait(limit);
|
||||
} finally {
|
||||
releaser();
|
||||
}
|
||||
}
|
||||
await p.all();
|
||||
Logger(`${procedurename} done.`);
|
||||
)(e));
|
||||
await Promise.all(processes);
|
||||
|
||||
Logger(`${procedureName} done.`);
|
||||
};
|
||||
|
||||
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
||||
@@ -1627,6 +1657,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
||||
Logger(`Check or pull from db:${e}`);
|
||||
await this.pullFile(e, filesStorage, false, null, false);
|
||||
Logger(`Check or pull from db:${e} OK`);
|
||||
});
|
||||
}
|
||||
if (!initialScan) {
|
||||
@@ -1693,7 +1724,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (ex.code && ex.code == "ENOENT") {
|
||||
//NO OP.
|
||||
} else {
|
||||
Logger(`error while delete filder:${folder.path}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`error while delete folder:${folder.path}`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
@@ -1763,7 +1794,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
// Conflicted item could not load, delete this.
|
||||
await this.localDatabase.deleteDBEntry(path, { rev: test._conflicts[0] });
|
||||
await this.pullFile(path, null, true);
|
||||
Logger(`could not get old revisions, automaticaly used newer one:${path}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL.NOTICE);
|
||||
return true;
|
||||
}
|
||||
// first,check for same contents
|
||||
@@ -1774,19 +1805,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
await this.localDatabase.deleteDBEntry(path, { rev: leaf.rev });
|
||||
await this.pullFile(path, null, true);
|
||||
Logger(`automaticaly merged:${path}`);
|
||||
Logger(`automatically merged:${path}`);
|
||||
return true;
|
||||
}
|
||||
if (this.settings.resolveConflictsByNewerFile) {
|
||||
const lmtime = ~~(leftLeaf.mtime / 1000);
|
||||
const rmtime = ~~(rightLeaf.mtime / 1000);
|
||||
const lMtime = ~~(leftLeaf.mtime / 1000);
|
||||
const rMtime = ~~(rightLeaf.mtime / 1000);
|
||||
let loser = leftLeaf;
|
||||
if (lmtime > rmtime) {
|
||||
if (lMtime > rMtime) {
|
||||
loser = rightLeaf;
|
||||
}
|
||||
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
|
||||
await this.pullFile(path, null, true);
|
||||
Logger(`Automaticaly merged (newerFileResolve) :${path}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Automatically merged (newerFileResolve) :${path}`, LOG_LEVEL.NOTICE);
|
||||
return true;
|
||||
}
|
||||
// make diff.
|
||||
@@ -1878,7 +1909,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await runWithLock("conflicted", false, async () => {
|
||||
const conflictCheckResult = await this.getConflictedStatus(file.path);
|
||||
if (conflictCheckResult === false) {
|
||||
//nothign to do.
|
||||
//nothing to do.
|
||||
return;
|
||||
}
|
||||
if (conflictCheckResult === true) {
|
||||
@@ -1896,6 +1927,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
||||
const targetFile = this.app.vault.getAbstractFileByPath(id2path(filename));
|
||||
if (!this.isTargetFile(id2path(filename))) return;
|
||||
if (targetFile == null) {
|
||||
//have to create;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
@@ -1971,6 +2003,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async updateIntoDB(file: TFile, initialScan?: boolean) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (shouldBeIgnored(file.path)) {
|
||||
return;
|
||||
}
|
||||
@@ -1984,9 +2017,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
content = await this.app.vault.read(file);
|
||||
datatype = "plain";
|
||||
}
|
||||
const fullpath = path2id(file.path);
|
||||
const fullPath = path2id(file.path);
|
||||
const d: LoadedEntry = {
|
||||
_id: fullpath,
|
||||
_id: fullPath,
|
||||
data: content,
|
||||
ctime: file.stat.ctime,
|
||||
mtime: file.stat.mtime,
|
||||
@@ -1997,16 +2030,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
};
|
||||
//upsert should locked
|
||||
const msg = `DB <- STORAGE (${datatype}) `;
|
||||
const isNotChanged = await runWithLock("file:" + fullpath, false, async () => {
|
||||
const isNotChanged = await runWithLock("file:" + fullPath, false, async () => {
|
||||
if (recentlyTouched(file)) {
|
||||
return true;
|
||||
}
|
||||
const old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
|
||||
const old = await this.localDatabase.getDBEntry(fullPath, null, false, false);
|
||||
if (old !== false) {
|
||||
const oldData = { data: old.data, deleted: old._deleted || old.deleted, };
|
||||
const newData = { data: d.data, deleted: d._deleted || d.deleted };
|
||||
if (JSON.stringify(oldData) == JSON.stringify(newData)) {
|
||||
Logger(msg + "Skipped (not changed) " + fullpath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
Logger(msg + "Skipped (not changed) " + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
return true;
|
||||
}
|
||||
// d._rev = old._rev;
|
||||
@@ -2018,23 +2051,24 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.queuedFiles = this.queuedFiles.map((e) => ({ ...e, ...(e.entry._id == d._id ? { done: true } : {}) }));
|
||||
|
||||
|
||||
Logger(msg + fullpath);
|
||||
Logger(msg + fullPath);
|
||||
if (this.settings.syncOnSave && !this.suspended) {
|
||||
await this.replicate();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFromDB(file: TFile) {
|
||||
const fullpath = file.path;
|
||||
Logger(`deleteDB By path:${fullpath}`);
|
||||
await this.deleteFromDBbyPath(fullpath);
|
||||
if (!this.isTargetFile(file)) return;
|
||||
const fullPath = file.path;
|
||||
Logger(`deleteDB By path:${fullPath}`);
|
||||
await this.deleteFromDBbyPath(fullPath);
|
||||
if (this.settings.syncOnSave && !this.suspended) {
|
||||
await this.replicate();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFromDBbyPath(fullpath: string) {
|
||||
await this.localDatabase.deleteDBEntry(fullpath);
|
||||
async deleteFromDBbyPath(fullPath: string) {
|
||||
await this.localDatabase.deleteDBEntry(fullPath);
|
||||
if (this.settings.syncOnSave && !this.suspended) {
|
||||
await this.replicate();
|
||||
}
|
||||
@@ -2294,7 +2328,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
return result;
|
||||
}
|
||||
|
||||
async storeInternaFileToDatabase(file: InternalFileInfo, forceWrite = false) {
|
||||
async storeInternalFileToDatabase(file: InternalFileInfo, forceWrite = false) {
|
||||
const id = filename2idInternalChunk(path2id(file.path));
|
||||
const contentBin = await this.app.vault.adapter.readBinary(file.path);
|
||||
const content = await arrayBufferToBase64(contentBin);
|
||||
@@ -2336,7 +2370,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteInternaFileOnDatabase(filename: string, forceWrite = false) {
|
||||
async deleteInternalFileOnDatabase(filename: string, forceWrite = false) {
|
||||
const id = filename2idInternalChunk(path2id(filename));
|
||||
const mtime = new Date().getTime();
|
||||
await runWithLock("file-" + id, false, async () => {
|
||||
@@ -2372,8 +2406,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
});
|
||||
}
|
||||
async ensureDirectoryEx(fullpath: string) {
|
||||
const pathElements = fullpath.split("/");
|
||||
async ensureDirectoryEx(fullPath: string) {
|
||||
const pathElements = fullPath.split("/");
|
||||
pathElements.pop();
|
||||
let c = "";
|
||||
for (const v of pathElements) {
|
||||
@@ -2383,7 +2417,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
} catch (ex) {
|
||||
// basically skip exceptions.
|
||||
if (ex.message && ex.message == "Folder already exists.") {
|
||||
// especialy this message is.
|
||||
// especially this message is.
|
||||
} else {
|
||||
Logger("Folder Create Error");
|
||||
Logger(ex);
|
||||
@@ -2392,7 +2426,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
c += "/";
|
||||
}
|
||||
}
|
||||
async extractInternaFileFromDatabase(filename: string, force = false) {
|
||||
async extractInternalFileFromDatabase(filename: string, force = false) {
|
||||
const isExists = await this.app.vault.adapter.exists(filename);
|
||||
const id = filename2idInternalChunk(path2id(filename));
|
||||
|
||||
@@ -2455,7 +2489,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
for (const row of docs.rows) {
|
||||
const doc = row.doc;
|
||||
if (!("_conflicts" in doc)) continue;
|
||||
if (isInteralChunk(row.id)) {
|
||||
if (isInternalChunk(row.id)) {
|
||||
await this.resolveConflictOnInternalFile(row.id);
|
||||
}
|
||||
}
|
||||
@@ -2466,15 +2500,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
// If there is no conflict, return with false.
|
||||
if (!("_conflicts" in doc)) return false;
|
||||
if (doc._conflicts.length == 0) return false;
|
||||
Logger(`Hidden file conflicetd:${id2filenameInternalChunk(id)}`);
|
||||
Logger(`Hidden file conflicted:${id2filenameInternalChunk(id)}`);
|
||||
const revA = doc._rev;
|
||||
const revB = doc._conflicts[0];
|
||||
|
||||
const revBdoc = await this.localDatabase.localDatabase.get(id, { rev: revB });
|
||||
// determine which revision sould been deleted.
|
||||
const revBDoc = await this.localDatabase.localDatabase.get(id, { rev: revB });
|
||||
// determine which revision should been deleted.
|
||||
// simply check modified time
|
||||
const mtimeA = ("mtime" in doc && doc.mtime) || 0;
|
||||
const mtimeB = ("mtime" in revBdoc && revBdoc.mtime) || 0;
|
||||
const mtimeB = ("mtime" in revBDoc && revBDoc.mtime) || 0;
|
||||
// Logger(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`);
|
||||
// console.log(`mtime:${mtimeA} - ${mtimeB}`);
|
||||
const delRev = mtimeA < mtimeB ? revA : revB;
|
||||
@@ -2508,8 +2542,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const fileCount = allFileNames.length;
|
||||
let processed = 0;
|
||||
let filesChanged = 0;
|
||||
const p = Parallels();
|
||||
const limit = 10;
|
||||
// count updated files up as like this below:
|
||||
// .obsidian: 2
|
||||
// .obsidian/workspace: 1
|
||||
@@ -2532,6 +2564,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
c = pieces.shift();
|
||||
}
|
||||
}
|
||||
const p = [] as Promise<void>[];
|
||||
const semaphore = Semaphore(15);
|
||||
// Cache update time information for files which have already been processed (mainly for files that were skipped due to the same content)
|
||||
let caches: { [key: string]: { storageMtime: number; docMtime: number } } = {};
|
||||
caches = await this.localDatabase.kvDB.get<{ [key: string]: { storageMtime: number; docMtime: number } }>("diff-caches-internal") || {};
|
||||
@@ -2542,12 +2576,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
const fileOnStorage = files.find(e => e.path == filename);
|
||||
const fileOnDatabase = filesOnDB.find(e => e._id == filename2idInternalChunk(id2path(filename)));
|
||||
const addProc = (p: () => Promise<void>): Promise<unknown> => {
|
||||
return p();
|
||||
const addProc = async (p: () => Promise<void>): Promise<void> => {
|
||||
const releaser = await semaphore.acquire(1);
|
||||
try {
|
||||
return p();
|
||||
} catch (ex) {
|
||||
Logger("Some process failed", logLevel)
|
||||
Logger(ex);
|
||||
} finally {
|
||||
releaser();
|
||||
}
|
||||
}
|
||||
const cache = filename in caches ? caches[filename] : { storageMtime: 0, docMtime: 0 };
|
||||
|
||||
p.add(addProc(async () => {
|
||||
p.push(addProc(async () => {
|
||||
if (fileOnStorage && fileOnDatabase) {
|
||||
// Both => Synchronize
|
||||
if (fileOnDatabase.mtime == cache.docMtime && fileOnStorage.mtime == cache.storageMtime) {
|
||||
@@ -2555,45 +2597,43 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
const nw = compareMTime(fileOnStorage.mtime, fileOnDatabase.mtime);
|
||||
if (nw > 0) {
|
||||
await this.storeInternaFileToDatabase(fileOnStorage);
|
||||
await this.storeInternalFileToDatabase(fileOnStorage);
|
||||
}
|
||||
if (nw < 0) {
|
||||
// skip if not extraction performed.
|
||||
if (!await this.extractInternaFileFromDatabase(filename)) return;
|
||||
if (!await this.extractInternalFileFromDatabase(filename)) return;
|
||||
}
|
||||
// If process successfly updated or file contents are same, update cache.
|
||||
// If process successfully updated or file contents are same, update cache.
|
||||
cache.docMtime = fileOnDatabase.mtime;
|
||||
cache.storageMtime = fileOnStorage.mtime;
|
||||
caches[filename] = cache;
|
||||
countUpdatedFolder(filename);
|
||||
} else if (!fileOnStorage && fileOnDatabase) {
|
||||
console.log("pushpull")
|
||||
if (direction == "push") {
|
||||
if (fileOnDatabase.deleted) return;
|
||||
await this.deleteInternaFileOnDatabase(filename);
|
||||
await this.deleteInternalFileOnDatabase(filename);
|
||||
} else if (direction == "pull") {
|
||||
if (await this.extractInternaFileFromDatabase(filename)) {
|
||||
if (await this.extractInternalFileFromDatabase(filename)) {
|
||||
countUpdatedFolder(filename);
|
||||
}
|
||||
} else if (direction == "safe") {
|
||||
if (fileOnDatabase.deleted) return
|
||||
if (await this.extractInternaFileFromDatabase(filename)) {
|
||||
if (await this.extractInternalFileFromDatabase(filename)) {
|
||||
countUpdatedFolder(filename);
|
||||
}
|
||||
}
|
||||
} else if (fileOnStorage && !fileOnDatabase) {
|
||||
await this.storeInternaFileToDatabase(fileOnStorage);
|
||||
await this.storeInternalFileToDatabase(fileOnStorage);
|
||||
} else {
|
||||
throw new Error("Invalid state on hidden file sync");
|
||||
// Something corrupted?
|
||||
}
|
||||
}));
|
||||
await p.wait(limit);
|
||||
}
|
||||
await p.all();
|
||||
await Promise.all(p);
|
||||
await this.localDatabase.kvDB.set("diff-caches-internal", caches);
|
||||
|
||||
// When files has been retreived from the database. they must be reloaded.
|
||||
// When files has been retrieved from the database. they must be reloaded.
|
||||
if (direction == "pull" && filesChanged != 0) {
|
||||
const configDir = normalizePath(this.app.vault.configDir);
|
||||
// Show notification to restart obsidian when something has been changed in configDir.
|
||||
@@ -2618,12 +2658,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
a.appendChild(a.createEl("a", null, (anchor) => {
|
||||
anchor.text = "HERE";
|
||||
anchor.addEventListener("click", async () => {
|
||||
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL.NOTICE, "pluin-reload-" + updatePluginId);
|
||||
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL.NOTICE, "plugin-reload-" + updatePluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(updatePluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(updatePluginId);
|
||||
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL.NOTICE, "pluin-reload-" + updatePluginId);
|
||||
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL.NOTICE, "plugin-reload-" + updatePluginId);
|
||||
});
|
||||
}))
|
||||
|
||||
@@ -2640,7 +2680,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
memoObject(updatedPluginKey, new Notice(fragment, 0))
|
||||
}
|
||||
setTrigger(updatedPluginKey + "-close", 20000, () => {
|
||||
const popup = retriveMemoObject<Notice>(updatedPluginKey)
|
||||
const popup = retrieveMemoObject<Notice>(updatedPluginKey)
|
||||
if (!popup) return;
|
||||
//@ts-ignore
|
||||
if (popup?.noticeEl?.isShown()) {
|
||||
@@ -2691,4 +2731,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
Logger(`Hidden files scanned: ${filesChanged} files had been modified`, logLevel, "sync_internal");
|
||||
}
|
||||
|
||||
isTargetFile(file: string | TAbstractFile) {
|
||||
if (file instanceof TFile) {
|
||||
return this.localDatabase.isTargetFile(file.path);
|
||||
} else if (typeof file == "string") {
|
||||
return this.localDatabase.isTargetFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PouchDB as PouchDB_ } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
|
||||
|
||||
const Pouch: PouchDB.Static = PouchDB_;
|
||||
export { Pouch as PouchDB };
|
||||
@@ -3,7 +3,7 @@ import { normalizePath } from "obsidian";
|
||||
import { path2id_base, id2path_base } from "./lib/src/utils";
|
||||
|
||||
// For backward compatibility, using the path for determining id.
|
||||
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
// The first slash will be deleted when the path is normalized.
|
||||
export function path2id(filename: string): string {
|
||||
const x = normalizePath(filename);
|
||||
@@ -63,7 +63,7 @@ export async function memoIfNotExist<T>(key: string, func: () => T | Promise<T>)
|
||||
}
|
||||
return memos[key] as T;
|
||||
}
|
||||
export function retriveMemoObject<T>(key: string): T | false {
|
||||
export function retrieveMemoObject<T>(key: string): T | false {
|
||||
if (key in memos) {
|
||||
return memos[key];
|
||||
} else {
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc, RemoteDBSettings, SYNCINFO_ID, SyncInfo } from "./lib/src/types";
|
||||
import { enableEncryption, resolveWithIgnoreKnownError } from "./lib/src/utils";
|
||||
import { PouchDB } from "./pouchdb-browser";
|
||||
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||
|
||||
export const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
||||
if (uri.startsWith("https://")) return true;
|
||||
if (uri.startsWith("http://")) return true;
|
||||
return false;
|
||||
};
|
||||
let last_post_successed = false;
|
||||
export const getLastPostFailedBySize = () => {
|
||||
return !last_post_successed;
|
||||
};
|
||||
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 connectRemoteCouchDBWithSetting = (settings: RemoteDBSettings, isMobile: boolean) =>
|
||||
connectRemoteCouchDB(
|
||||
settings.couchDB_URI + (settings.couchDB_DBNAME == "" ? "" : "/" + settings.couchDB_DBNAME),
|
||||
{
|
||||
username: settings.couchDB_USER,
|
||||
password: settings.couchDB_PASSWORD,
|
||||
},
|
||||
settings.disableRequestURI || isMobile,
|
||||
settings.encrypt ? settings.passphrase : settings.encrypt
|
||||
);
|
||||
|
||||
const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> => {
|
||||
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
||||
if (uri.toLowerCase() != uri) return "Remote URI and database name cound not contain capital letters.";
|
||||
if (uri.indexOf(" ") !== -1) return "Remote URI and database name cound not contain spaces.";
|
||||
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 = {
|
||||
adapter: "http",
|
||||
auth,
|
||||
fetch: async function (url: string | Request, opts: RequestInit) {
|
||||
let size = "";
|
||||
const localURL = url.toString().substring(uri.length);
|
||||
const method = opts.method ?? "GET";
|
||||
if (opts.body) {
|
||||
const opts_length = opts.body.toString().length;
|
||||
if (opts_length > 1024 * 1024 * 10) {
|
||||
// over 10MB
|
||||
if (uri.contains(".cloudantnosqldb.")) {
|
||||
last_post_successed = false;
|
||||
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
||||
throw new Error("This request should fail on IBM Cloudant.");
|
||||
}
|
||||
}
|
||||
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.DEBUG);
|
||||
|
||||
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 {
|
||||
const responce: Response = await fetch(url, opts);
|
||||
if (method == "POST" || method == "PUT") {
|
||||
last_post_successed = responce.ok;
|
||||
} else {
|
||||
last_post_successed = true;
|
||||
}
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${responce.status}`, LOG_LEVEL.DEBUG);
|
||||
return responce;
|
||||
} 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;
|
||||
}
|
||||
// return await fetch(url, opts);
|
||||
},
|
||||
};
|
||||
|
||||
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
|
||||
if (passphrase && typeof passphrase === "string") {
|
||||
enableEncryption(db, passphrase);
|
||||
}
|
||||
try {
|
||||
const info = await db.info();
|
||||
return { db: db, info: info };
|
||||
} catch (ex) {
|
||||
let msg = `${ex.name}:${ex.message}`;
|
||||
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
|
||||
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
|
||||
}
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
// check the version of remote.
|
||||
// if remote is higher than current(or specified) version, return false.
|
||||
export const checkRemoteVersion = async (db: PouchDB.Database, migrate: (from: number, to: number) => Promise<boolean>, barrier: number = VER): Promise<boolean> => {
|
||||
try {
|
||||
const versionInfo = (await db.get(VERSIONINFO_DOCID)) as EntryVersionInfo;
|
||||
if (versionInfo.type != "versioninfo") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = versionInfo.version;
|
||||
if (version < barrier) {
|
||||
const versionUpResult = await migrate(version, barrier);
|
||||
if (versionUpResult) {
|
||||
await bumpRemoteVersion(db);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (version == barrier) return true;
|
||||
return false;
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
if (await bumpRemoteVersion(db)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
};
|
||||
export const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): Promise<boolean> => {
|
||||
const vi: EntryVersionInfo = {
|
||||
_id: VERSIONINFO_DOCID,
|
||||
version: barrier,
|
||||
type: "versioninfo",
|
||||
};
|
||||
const versionInfo = (await resolveWithIgnoreKnownError(db.get(VERSIONINFO_DOCID), vi)) as EntryVersionInfo;
|
||||
if (versionInfo.type != "versioninfo") {
|
||||
return false;
|
||||
}
|
||||
vi._rev = versionInfo._rev;
|
||||
await db.put(vi);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const checkSyncInfo = async (db: PouchDB.Database): Promise<boolean> => {
|
||||
try {
|
||||
const syncinfo = (await db.get(SYNCINFO_ID)) as SyncInfo;
|
||||
console.log(syncinfo);
|
||||
// if we could decrypt the doc, it must be ok.
|
||||
return true;
|
||||
} catch (ex) {
|
||||
if (ex.status && ex.status == 404) {
|
||||
const randomStrSrc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const temp = [...Array(30)]
|
||||
.map((e) => Math.floor(Math.random() * randomStrSrc.length))
|
||||
.map((e) => randomStrSrc[e])
|
||||
.join("");
|
||||
const newSyncInfo: SyncInfo = {
|
||||
_id: SYNCINFO_ID,
|
||||
type: "syncinfo",
|
||||
data: temp,
|
||||
};
|
||||
if (await db.put(newSyncInfo)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
console.dir(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2,15 +2,26 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"target": "ES2018",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
// "importsNotUsedAsValues": "error",
|
||||
"importHelpers": true,
|
||||
"importHelpers": false,
|
||||
"alwaysStrict": true,
|
||||
"lib": ["es2018", "DOM", "ES5", "ES6", "ES7"]
|
||||
"lib": [
|
||||
"es2018",
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7",
|
||||
"es2019.array"
|
||||
]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["pouchdb-browser-webpack"]
|
||||
}
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"pouchdb-browser-webpack"
|
||||
]
|
||||
}
|
||||
26
updates.md
26
updates.md
@@ -1,3 +1,27 @@
|
||||
### 0.14.1
|
||||
- The target selecting filter was implemented.
|
||||
Now we can set what files are synchronised by regular expression.
|
||||
- We can configure the size of chunks.
|
||||
We can use larger chunks to improve performance.
|
||||
(This feature can not be used with IBM Cloudant)
|
||||
- Read chunks online.
|
||||
Now we can synchronise only metadata and retrieve chunks on demand. It reduces local database size and time for replication.
|
||||
- Added this note.
|
||||
- Use local chunks in preference to remote them if present,
|
||||
|
||||
#### Recommended configuration for Self-hosted CouchDB
|
||||
- Set chunk size to around 100 to 250 (10MB - 25MB per chunk)
|
||||
- *Set batch size to 100 and batch limit to 20 (0.14.2)*
|
||||
- Be sure to `Read chunks online` checked.
|
||||
|
||||
#### Minors
|
||||
- 0.14.2 Fixed issue about retrieving files if synchronisation has been interrupted or failed
|
||||
- 0.14.3 New test items have been added to `Check database configuration`.
|
||||
- 0.14.4 Fixed issue of importing configurations.
|
||||
- 0.14.5 Auto chunk size adjusting implemented.
|
||||
- 0.14.6 Change Target to ES2018
|
||||
- 0.14.7 Refactor and fix typos.
|
||||
- 0.14.8 Refactored again. There should be no change in behaviour, but please let me know if there is any.
|
||||
### 0.13.0
|
||||
|
||||
- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded.
|
||||
@@ -14,4 +38,4 @@
|
||||
- Now, we can synchronise hidden files that conflicted on each devices.
|
||||
- We can search for conflicting docs.
|
||||
- Pending processes can now be run at any time.
|
||||
- Performance improved on synchronising large numbers of files at once.
|
||||
- Performance improved on synchronising large numbers of files at once.
|
||||
|
||||
Reference in New Issue
Block a user