Compare commits

...

8 Commits

Author SHA1 Message Date
vorotamoroz
dca8e4b2a4 bump 2024-05-10 11:38:03 +01:00
vorotamoroz
89de2dcc37 Fixed:
- No longer missing tasks which have queued as the same key (e.g., for the same operation to the same file).
- Some trivial issues have been fixed.
New feature:
- Reloading Obsidian can be scheduled until that file and database operations are stable.
2024-05-10 11:33:59 +01:00
vorotamoroz
172b08dbb3 bump 2024-05-08 23:57:19 +09:00
vorotamoroz
d518a3fc1b Fixed:
- Now the remote chunks could be decrypted even if we are using `Incubate chunks in Document`. (The note of 0.23.6 has been fixed).
- Chunk retrieving with `Incubate chunks in document` got more efficiently.
- No longer task processor misses the completed tasks.
- Replication is no longer started automatically during changes in window visibility (e.g., task switching on the desktop) when off-focused.
2024-05-08 23:56:29 +09:00
vorotamoroz
c6ed867498 bump 2024-05-07 12:59:55 +01:00
vorotamoroz
4f4923e977 New feature:
- Now we can check configuration mismatching between clients before synchronisation.
- Now we can perform remote database compaction from the `Maintenance` pane.
Fixed:
- We can detect the bucket could not be reachable.
2024-05-07 12:55:48 +01:00
vorotamoroz
a5ebf29b3d Merge pull request #417 from MichaelBrunn3r/translation
fix: Grammar issues in settings page
2024-05-07 20:26:59 +09:00
Michael Brunner
cbf5023593 fix: Grammar issues in settings page 2024-05-04 12:34:53 +02:00
9 changed files with 321 additions and 157 deletions

View File

@@ -1,7 +1,7 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Self-hosted LiveSync", "name": "Self-hosted LiveSync",
"version": "0.23.4", "version": "0.23.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",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.23.3", "version": "0.23.7",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.23.3", "version": "0.23.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.556.0", "@aws-sdk/client-s3": "^3.556.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.23.4", "version": "0.23.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",

View File

@@ -10,7 +10,7 @@ import { readString, decodeBinary, arrayBufferToBase64, digestHash } from "../li
import { serialized } from "../lib/src/concurrency/lock.ts"; import { serialized } from "../lib/src/concurrency/lock.ts";
import { LiveSyncCommands } from "./LiveSyncCommands.ts"; import { LiveSyncCommands } from "./LiveSyncCommands.ts";
import { stripAllPrefixes } from "../lib/src/string_and_binary/path.ts"; import { stripAllPrefixes } from "../lib/src/string_and_binary/path.ts";
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts"; import { PeriodicProcessor, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../common/utils.ts";
import { PluginDialogModal } from "../common/dialogs.ts"; import { PluginDialogModal } from "../common/dialogs.ts";
import { JsonResolveModal } from "../ui/JsonResolveModal.ts"; import { JsonResolveModal } from "../ui/JsonResolveModal.ts";
import { QueueProcessor } from '../lib/src/concurrency/processor.ts'; import { QueueProcessor } from '../lib/src/concurrency/processor.ts';
@@ -466,12 +466,7 @@ export class ConfigSync extends LiveSyncCommands {
Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id);
} }
} else if (data.category == "CONFIG") { } else if (data.category == "CONFIG") {
scheduleTask("configReload", 250, async () => { this.plugin.askReload();
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
// @ts-ignore
this.app.commands.executeCommandById("app:reload")
}
})
} }
return true; return true;
} catch (ex) { } catch (ex) {
@@ -684,6 +679,7 @@ export class ConfigSync extends LiveSyncCommands {
children: [], children: [],
deleted: false, deleted: false,
type: "newnote", type: "newnote",
eden: {}
}; };
} else { } else {
if (old.mtime == mtime) { if (old.mtime == mtime) {

View File

@@ -432,13 +432,14 @@ export class HiddenFileSync extends LiveSyncCommands {
// If something changes left, notify for reloading Obsidian. // If something changes left, notify for reloading Obsidian.
if (updatedCount != 0) { if (updatedCount != 0) {
this.plugin.askInPopup(`updated-any-hidden`, `Hidden files have been synchronized, Press {HERE} to reload Obsidian, or press elsewhere to dismiss this message.`, (anchor) => { if (!this.plugin.isReloadingScheduled) {
anchor.text = "HERE"; this.plugin.askInPopup(`updated-any-hidden`, `Hidden files have been synchronised, Press {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`, (anchor) => {
anchor.addEventListener("click", () => { anchor.text = "HERE";
// @ts-ignore anchor.addEventListener("click", () => {
this.app.commands.executeCommandById("app:reload"); this.plugin.scheduleAppReload();
});
}); });
}); }
} }
} }
} }
@@ -471,6 +472,7 @@ export class HiddenFileSync extends LiveSyncCommands {
children: [], children: [],
deleted: false, deleted: false,
type: "newnote", type: "newnote",
eden: {},
}; };
} else { } else {
if (await isDocContentSame(readAsBlob(old), content) && !forceWrite) { if (await isDocContentSame(readAsBlob(old), content) && !forceWrite) {
@@ -521,6 +523,7 @@ export class HiddenFileSync extends LiveSyncCommands {
children: [], children: [],
deleted: true, deleted: true,
type: "newnote", type: "newnote",
eden: {}
}; };
} else { } else {
// Remove all conflicted before deleting. // Remove all conflicted before deleting.

Submodule src/lib updated: 1417452fec...13f8370ef5

View File

@@ -2,9 +2,9 @@ const isDebug = false;
import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch, stringifyYaml, parseYaml } from "./deps"; import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch, stringifyYaml, parseYaml } from "./deps";
import { Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl, type MarkdownFileInfo } from "./deps"; import { Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl, type MarkdownFileInfo } from "./deps";
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE, type SavingEntry, MISSING_OR_ERROR, NOT_CONFLICTED, AUTO_MERGED, CANCELLED, LEAVE_TO_SUBSEQUENT, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, REMOTE_MINIO, REMOTE_COUCHDB, type BucketSyncSetting, } from "./lib/src/common/types.ts"; import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE, type SavingEntry, MISSING_OR_ERROR, NOT_CONFLICTED, AUTO_MERGED, CANCELLED, LEAVE_TO_SUBSEQUENT, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, REMOTE_MINIO, REMOTE_COUCHDB, type BucketSyncSetting, TweakValuesShouldMatchedTemplate, confName, type TweakValues, } from "./lib/src/common/types.ts";
import { type InternalFileInfo, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./common/types.ts"; import { type InternalFileInfo, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./common/types.ts";
import { arrayToChunkedArray, createBlob, delay, determineTypeFromBlob, fireAndForget, getDocData, isAnyNote, isDocContentSame, isObjectDifferent, readContent, sendValue, throttle, type SimpleStore } from "./lib/src/common/utils.ts"; import { arrayToChunkedArray, createBlob, delay, determineTypeFromBlob, escapeMarkdownValue, extractObject, fireAndForget, getDocData, isAnyNote, isDocContentSame, isObjectDifferent, readContent, sendValue, throttle, type SimpleStore } from "./lib/src/common/utils.ts";
import { Logger, setGlobalLogFunction } from "./lib/src/common/logger.ts"; import { Logger, setGlobalLogFunction } from "./lib/src/common/logger.ts";
import { PouchDB } from "./lib/src/pouchdb/pouchdb-browser.js"; import { PouchDB } from "./lib/src/pouchdb/pouchdb-browser.js";
import { ConflictResolveModal } from "./ui/ConflictResolveModal.ts"; import { ConflictResolveModal } from "./ui/ConflictResolveModal.ts";
@@ -32,7 +32,7 @@ import { LogPaneView, VIEW_TYPE_LOG } from "./ui/LogPaneView.ts";
import { LRUCache } from "./lib/src/memory/LRUCache.ts"; import { LRUCache } from "./lib/src/memory/LRUCache.ts";
import { SerializedFileAccess } from "./storages/SerializedFileAccess.js"; import { SerializedFileAccess } from "./storages/SerializedFileAccess.js";
import { QueueProcessor } from "./lib/src/concurrency/processor.js"; import { QueueProcessor } from "./lib/src/concurrency/processor.js";
import { reactive, reactiveSource } from "./lib/src/dataobject/reactive.js"; import { reactive, reactiveSource, type ReactiveValue } from "./lib/src/dataobject/reactive.js";
import { initializeStores } from "./common/stores.js"; import { initializeStores } from "./common/stores.js";
import { JournalSyncMinio } from "./lib/src/replication/journal/objectstore/JournalSyncMinio.js"; import { JournalSyncMinio } from "./lib/src/replication/journal/objectstore/JournalSyncMinio.js";
import { LiveSyncJournalReplicator, type LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicator.js"; import { LiveSyncJournalReplicator, type LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicator.js";
@@ -1374,6 +1374,7 @@ We can perform a command in this file.
} else { } else {
// suspend all temporary. // suspend all temporary.
if (this.suspended) return; if (this.suspended) return;
if (!this.hasFocus) return;
await Promise.all(this.addOns.map(e => e.onResume())); await Promise.all(this.addOns.map(e => e.onResume()));
if (this.settings.remoteType == REMOTE_COUCHDB) { if (this.settings.remoteType == REMOTE_COUCHDB) {
if (this.settings.liveSync) { if (this.settings.liveSync) {
@@ -1421,55 +1422,62 @@ We can perform a command in this file.
} }
async handleFileEvent(queue: FileEventItem): Promise<any> { async handleFileEvent(queue: FileEventItem): Promise<any> {
const file = queue.args.file; const file = queue.args.file;
const key = `file-last-proc-${queue.type}-${file.path}`; const lockKey = `handleFile:${file.path}`;
const last = Number(await this.kvDB.get(key) || 0); return await serialized(lockKey, async () => {
let mtime = file.mtime; const key = `file-last-proc-${queue.type}-${file.path}`;
if (queue.type == "DELETE") { const last = Number(await this.kvDB.get(key) || 0);
await this.deleteFromDBbyPath(file.path); let mtime = file.mtime;
mtime = file.mtime - 1; if (queue.type == "DELETE") {
const keyD1 = `file-last-proc-CREATE-${file.path}`; await this.deleteFromDBbyPath(file.path);
const keyD2 = `file-last-proc-CHANGED-${file.path}`; mtime = file.mtime - 1;
await this.kvDB.set(keyD1, mtime); const keyD1 = `file-last-proc-CREATE-${file.path}`;
await this.kvDB.set(keyD2, mtime); const keyD2 = `file-last-proc-CHANGED-${file.path}`;
} else if (queue.type == "INTERNAL") {
await this.addOnHiddenFileSync.watchVaultRawEventsAsync(file.path);
await this.addOnConfigSync.watchVaultRawEventsAsync(file.path);
} else {
const targetFile = this.vaultAccess.getAbstractFileByPath(file.path);
if (!(targetFile instanceof TFile)) {
Logger(`Target file was not found: ${file.path}`, LOG_LEVEL_INFO);
return;
}
if (file.mtime == last) {
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
// const cache = queue.args.cache;
if (queue.type == "CREATE" || queue.type == "CHANGED") {
fireAndForget(() => this.checkAndApplySettingFromMarkdown(queue.args.file.path, true));
const keyD1 = `file-last-proc-DELETED-${file.path}`;
await this.kvDB.set(keyD1, mtime); await this.kvDB.set(keyD1, mtime);
if (!await this.updateIntoDB(targetFile, undefined)) { await this.kvDB.set(keyD2, mtime);
Logger(`STORAGE -> DB: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL_INFO); } else if (queue.type == "INTERNAL") {
// cancel running queues and remove one of atomic operation await this.addOnHiddenFileSync.watchVaultRawEventsAsync(file.path);
this.cancelRelativeEvent(queue); await this.addOnConfigSync.watchVaultRawEventsAsync(file.path);
} else {
const targetFile = this.vaultAccess.getAbstractFileByPath(file.path);
if (!(targetFile instanceof TFile)) {
Logger(`Target file was not found: ${file.path}`, LOG_LEVEL_INFO);
return; return;
} }
if (file.mtime == last) {
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
// const cache = queue.args.cache;
if (queue.type == "CREATE" || queue.type == "CHANGED") {
fireAndForget(() => this.checkAndApplySettingFromMarkdown(queue.args.file.path, true));
const keyD1 = `file-last-proc-DELETED-${file.path}`;
await this.kvDB.set(keyD1, mtime);
if (!await this.updateIntoDB(targetFile, undefined)) {
Logger(`STORAGE -> DB: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL_INFO);
// cancel running queues and remove one of atomic operation
this.cancelRelativeEvent(queue);
return;
}
}
if (queue.type == "RENAME") {
// Obsolete
await this.watchVaultRenameAsync(targetFile, queue.args.oldPath);
}
} }
if (queue.type == "RENAME") { await this.kvDB.set(key, mtime);
// Obsolete });
await this.watchVaultRenameAsync(targetFile, queue.args.oldPath);
}
}
await this.kvDB.set(key, mtime);
} }
pendingFileEventCount = reactiveSource(0); pendingFileEventCount = reactiveSource(0);
processingFileEventCount = reactiveSource(0); processingFileEventCount = reactiveSource(0);
fileEventQueue = fileEventQueue =
new QueueProcessor( new QueueProcessor(
(items: FileEventItem[]) => this.handleFileEvent(items[0]), async (items: FileEventItem[]) => {
await this.handleFileEvent(items[0]);
return []
}
,
{ suspended: true, batchSize: 1, concurrentLimit: 5, delay: 100, yieldThreshold: FileWatchEventQueueMax, totalRemainingReactiveSource: this.pendingFileEventCount, processingEntitiesReactiveSource: this.processingFileEventCount } { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 100, yieldThreshold: FileWatchEventQueueMax, totalRemainingReactiveSource: this.pendingFileEventCount, processingEntitiesReactiveSource: this.processingFileEventCount }
).replaceEnqueueProcessor((items, newItem) => this.queueNextFileEvent(items, newItem)); ).replaceEnqueueProcessor((items, newItem) => this.queueNextFileEvent(items, newItem));
@@ -1893,7 +1901,7 @@ We can perform a command in this file.
observeForLogs() { observeForLogs() {
const padSpaces = `\u{2007}`.repeat(10); const padSpaces = `\u{2007}`.repeat(10);
// const emptyMark = `\u{2003}`; // const emptyMark = `\u{2003}`;
const rerenderTimer = new Map<string, [ReturnType<typeof setTimeout>, number]>; const rerenderTimer = new Map<string, [ReturnType<typeof setTimeout>, number]>();
const tick = reactiveSource(0); const tick = reactiveSource(0);
function padLeftSp(num: number, mark: string) { function padLeftSp(num: number, mark: string) {
const numLen = `${num}`.length + 1; const numLen = `${num}`.length + 1;
@@ -2004,9 +2012,9 @@ We can perform a command in this file.
}; };
}) })
const statusBarLabels = reactive(() => { const statusBarLabels = reactive(() => {
const scheduleMessage = this.isReloadingScheduled ? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n` : "";
const { message } = statusLineLabel.value; const { message } = statusLineLabel.value;
const status = this.statusLog.value; const status = scheduleMessage + this.statusLog.value;
return { return {
message, status message, status
} }
@@ -2052,58 +2060,114 @@ We can perform a command in this file.
await this.loadQueuedFiles(); await this.loadQueuedFiles();
const ret = await this.replicator.openReplication(this.settings, false, showMessage, false); const ret = await this.replicator.openReplication(this.settings, false, showMessage, false);
if (!ret) { if (!ret) {
if (this.replicator.remoteLockedAndDeviceNotAccepted) { if (this.replicator.tweakSettingsMismatched) {
if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) { const remoteSettings = this.replicator.mismatchedTweakValues;
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); const mustSettings = remoteSettings.map(e => extractObject(TweakValuesShouldMatchedTemplate, e));
await skipIfDuplicated("cleanup", async () => { const items = Object.entries(TweakValuesShouldMatchedTemplate);
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true); // Making tables:
const message = `The remote database has been cleaned up. let table = `| Value name | Ours | ${mustSettings.map((_, i) => `Remote ${i + 1} |`).join("")}\n` +
`|: --- |: --- :${`|: --- :`.repeat(mustSettings.length)}|\n`
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const value = mustSettings.map(e => e[key]);
table += `| ${confName(key)} | ${escapeMarkdownValue(this.settings[key])} | ${value.map((v) => `${escapeMarkdownValue(v)} |`).join("")}\n`;
}
const message = `
Configuration mismatching between the clients has been detected.
This can be harmful or extra capacity consumption. We have to make these value unified.
Configured values:
${table}
Please select a unification method.
However, even if we answer that you will \`Use mine\`, we will be prompted to accept it again on the other device and have to decide accept or not.`;
//TODO: apply this settings.
const CHOICE_USE_REMOTE = "Use Remote ";
const CHOICE_USR_MINE = "Use ours";
const CHOICE_DISMISS = "Dismiss";
// const ourConfig = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
const CHOICE_AND_VALUES = [
...mustSettings.map((e, i) => [`${CHOICE_USE_REMOTE} ${i + 1}`, e]),
[CHOICE_USR_MINE, true],
[CHOICE_DISMISS, false]
]
const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record<string, TweakValues | boolean>;
const retKey = await confirmWithMessage(this, "Locked", message, Object.keys(CHOICES), CHOICE_DISMISS, 60);
if (!retKey) return;
const conf = CHOICES[retKey];
if (!conf) {
return;
}
if (conf === true) {
await this.replicator.resetRemoteTweakSettings(this.settings);
Logger(`Tweak values on the remote server have been cleared, and will be overwritten in next synchronisation.`, LOG_LEVEL_NOTICE);
return;
}
if (conf) {
this.settings = { ...this.settings, ...conf };
await this.saveSettingData();
Logger(`Tweak Values have been overwritten by the chosen one.`, LOG_LEVEL_NOTICE);
return;
}
} else {
if (this.replicator.remoteLockedAndDeviceNotAccepted) {
if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
await skipIfDuplicated("cleanup", async () => {
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
const message = `The remote database has been cleaned up.
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device. To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
However, If there are many chunks to be deleted, maybe fetching again is faster. However, If there are many chunks to be deleted, maybe fetching again is faster.
We will lose the history of this device if we fetch the remote database again. We will lose the history of this device if we fetch the remote database again.
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.` Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`
const CHOICE_FETCH = "Fetch again"; const CHOICE_FETCH = "Fetch again";
const CHOICE_CLEAN = "Cleanup"; const CHOICE_CLEAN = "Cleanup";
const CHOICE_DISMISS = "Dismiss"; const CHOICE_DISMISS = "Dismiss";
const ret = await confirmWithMessage(this, "Cleaned", message, [CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS], CHOICE_DISMISS, 30); const ret = await confirmWithMessage(this, "Cleaned", message, [CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS], CHOICE_DISMISS, 30);
if (ret == CHOICE_FETCH) { if (ret == CHOICE_FETCH) {
await performRebuildDB(this, "localOnly"); await performRebuildDB(this, "localOnly");
}
if (ret == CHOICE_CLEAN) {
const replicator = this.getReplicator();
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(this.settings, this.getIsMobile(), true);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL_NOTICE);
return false;
} }
if (ret == CHOICE_CLEAN) {
const replicator = this.getReplicator();
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(this.settings, this.getIsMobile(), true);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL_NOTICE);
return false;
}
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.hashCaches.clear();
// Perform the synchronisation once.
if (await this.replicator.openReplication(this.settings, false, showMessage, true)) {
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false); await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.hashCaches.clear(); this.localDatabase.hashCaches.clear();
await this.getReplicator().markRemoteResolved(this.settings); // Perform the synchronisation once.
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO) if (await this.replicator.openReplication(this.settings, false, showMessage, true)) {
} else { await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
Logger("Replication has been cancelled. Please try it again.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO) await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
} this.localDatabase.hashCaches.clear();
await this.getReplicator().markRemoteResolved(this.settings);
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO)
} else {
Logger("Replication has been cancelled. Please try it again.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO)
}
} }
}); });
} else { } else {
const message = ` const message = `
The remote database has been rebuilt. The remote database has been rebuilt.
To synchronize, this device must fetch everything again once. To synchronize, this device must fetch everything again once.
Or if you are sure know what had been happened, we can unlock the database from the setting dialog. Or if you are sure know what had been happened, we can unlock the database from the setting dialog.
` `
const CHOICE_FETCH = "Fetch again"; const CHOICE_FETCH = "Fetch again";
const CHOICE_DISMISS = "Dismiss"; const CHOICE_DISMISS = "Dismiss";
const ret = await confirmWithMessage(this, "Locked", message, [CHOICE_FETCH, CHOICE_DISMISS], CHOICE_DISMISS, 10); const ret = await confirmWithMessage(this, "Locked", message, [CHOICE_FETCH, CHOICE_DISMISS], CHOICE_DISMISS, 10);
if (ret == CHOICE_FETCH) { if (ret == CHOICE_FETCH) {
await performRebuildDB(this, "localOnly"); await performRebuildDB(this, "localOnly");
}
} }
} }
} }
@@ -3117,5 +3181,61 @@ Or if you are sure know what had been happened, we can unlock the database from
// @ts-ignore // @ts-ignore
this.app.commands.executeCommandById(id) this.app.commands.executeCommandById(id)
} }
_totalProcessingCount?: ReactiveValue<number>;
get isReloadingScheduled() {
return this._totalProcessingCount !== undefined;
}
askReload(message?: string) {
if (this.isReloadingScheduled) {
Logger(`Reloading is already scheduled`, LOG_LEVEL_VERBOSE);
return;
}
scheduleTask("configReload", 250, async () => {
const RESTART_NOW = "Yes, restart immediately";
const RESTART_AFTER_STABLE = "Yes, schedule a restart after stabilisation";
const RETRY_LATER = "No, Leave it to me";
const ret = await askSelectString(this.app, message || "Do you want to restart and reload Obsidian now?", [RESTART_AFTER_STABLE, RESTART_NOW, RETRY_LATER]);
if (ret == RESTART_NOW) {
this.performAppReload();
} else if (ret == RESTART_AFTER_STABLE) {
this.scheduleAppReload();
}
})
}
scheduleAppReload() {
if (!this._totalProcessingCount) {
const __tick = reactiveSource(0);
this._totalProcessingCount = reactive(() => {
const dbCount = this.databaseQueueCount.value;
const replicationCount = this.replicationResultCount.value;
const storageApplyingCount = this.storageApplyingCount.value;
const chunkCount = collectingChunks.value;
const pluginScanCount = pluginScanningCount.value;
const hiddenFilesCount = hiddenFilesEventCount.value + hiddenFilesProcessingCount.value;
const conflictProcessCount = this.conflictProcessQueueCount.value;
const e = this.pendingFileEventCount.value;
const proc = this.processingFileEventCount.value;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const __ = __tick.value;
return dbCount + replicationCount + storageApplyingCount + chunkCount + pluginScanCount + hiddenFilesCount + conflictProcessCount + e + proc;
})
this.registerInterval(setInterval(() => {
__tick.value++;
}, 1000) as unknown as number);
let stableCheck = 3;
this._totalProcessingCount.onChanged(e => {
if (e.value == 0) {
if (stableCheck-- <= 0) {
this.performAppReload();
}
Logger(`Obsidian will be restarted soon! (Within ${stableCheck} seconds)`, LOG_LEVEL_NOTICE, "restart-notice");
} else {
stableCheck = 3;
}
})
}
}
} }

View File

@@ -19,7 +19,8 @@ import {
REMOTE_MINIO, REMOTE_MINIO,
type BucketSyncSetting, type BucketSyncSetting,
type RemoteType, type RemoteType,
PREFERRED_JOURNAL_SYNC PREFERRED_JOURNAL_SYNC,
confName
} from "../lib/src/common/types.ts"; } from "../lib/src/common/types.ts";
import { createBlob, delay, extractObject, isDocContentSame, readAsBlob } from "../lib/src/common/utils.ts"; import { createBlob, delay, extractObject, isDocContentSame, readAsBlob } from "../lib/src/common/utils.ts";
import { versionNumberString2Number } from "../lib/src/string_and_binary/strbin.ts"; import { versionNumberString2Number } from "../lib/src/string_and_binary/strbin.ts";
@@ -27,7 +28,7 @@ import { Logger } from "../lib/src/common/logger.ts";
import { checkSyncInfo, isCloudantURI } from "../lib/src/pouchdb/utils_couchdb.ts"; import { checkSyncInfo, isCloudantURI } from "../lib/src/pouchdb/utils_couchdb.ts";
import { testCrypt } from "../lib/src/encryption/e2ee_v2.ts"; import { testCrypt } from "../lib/src/encryption/e2ee_v2.ts";
import ObsidianLiveSyncPlugin from "../main.ts"; import ObsidianLiveSyncPlugin from "../main.ts";
import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "../common/utils.ts"; import { askYesNo, performRebuildDB, requestToCouchDB } from "../common/utils.ts";
import { request, type ButtonComponent, TFile } from "obsidian"; import { request, type ButtonComponent, TFile } from "obsidian";
import { shouldBeIgnored } from "../lib/src/string_and_binary/path.ts"; import { shouldBeIgnored } from "../lib/src/string_and_binary/path.ts";
import MultipleRegExpControl from './components/MultipleRegExpControl.svelte'; import MultipleRegExpControl from './components/MultipleRegExpControl.svelte';
@@ -50,15 +51,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await replicator.tryConnectRemote(trialSetting); await replicator.tryConnectRemote(trialSetting);
} }
askReload(message?: string) {
scheduleTask("configReload", 250, async () => {
if (await askYesNo(this.app, message || "Do you want to restart and reload Obsidian now?") == "yes") {
// @ts-ignore
this.app.commands.executeCommandById("app:reload")
}
})
}
closeSetting() { closeSetting() {
// @ts-ignore // @ts-ignore
this.plugin.app.setting.close() this.plugin.app.setting.close()
@@ -137,7 +129,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
const tmpDiv = createSpan(); const tmpDiv = createSpan();
tmpDiv.addClass("sls-header-button"); tmpDiv.addClass("sls-header-button");
tmpDiv.innerHTML = `<button> OK, I read all. </button>`; tmpDiv.innerHTML = `<button> OK, I read everything. </button>`;
if (lastVersion > this.plugin.settings.lastReadUpdates) { if (lastVersion > this.plugin.settings.lastReadUpdates) {
const informationButtonDiv = h3El.appendChild(tmpDiv); const informationButtonDiv = h3El.appendChild(tmpDiv);
informationButtonDiv.querySelector("button")?.addEventListener("click", async () => { informationButtonDiv.querySelector("button")?.addEventListener("click", async () => {
@@ -211,26 +203,26 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
if (!this.plugin.settings.isConfigured) { if (!this.plugin.settings.isConfigured) {
new Setting(setupWizardEl) new Setting(setupWizardEl)
.setName("Enable LiveSync on this device as the set-up was completed manually") .setName("Enable LiveSync on this device as the setup was completed manually")
.addButton((text) => { .addButton((text) => {
text.setButtonText("Enable").onClick(async () => { text.setButtonText("Enable").onClick(async () => {
this.plugin.settings.isConfigured = true; this.plugin.settings.isConfigured = true;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
this.askReload(); this.plugin.askReload();
}) })
}) })
} }
if (this.plugin.settings.isConfigured) { if (this.plugin.settings.isConfigured) {
new Setting(setupWizardEl) new Setting(setupWizardEl)
.setName("Discard exist settings and databases") .setName("Discard existing settings and databases")
.addButton((text) => { .addButton((text) => {
text.setButtonText("Discard").onClick(async () => { text.setButtonText("Discard").onClick(async () => {
if (await askYesNo(this.plugin.app, "Do you really want to discard exist settings and databases?") == "yes") { if (await askYesNo(this.plugin.app, "Do you really want to discard existing settings and databases?") == "yes") {
this.plugin.settings = { ...DEFAULT_SETTINGS }; this.plugin.settings = { ...DEFAULT_SETTINGS };
await this.plugin.saveSettingData(); await this.plugin.saveSettingData();
await this.plugin.resetLocalDatabase(); await this.plugin.resetLocalDatabase();
// await this.plugin.initializeDatabase(); // await this.plugin.initializeDatabase();
this.askReload(); this.plugin.askReload();
} }
}).setWarning() }).setWarning()
}) })
@@ -255,7 +247,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
try { try {
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`); remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
} catch (ex: any) { } catch (ex: any) {
remoteTroubleShootMDSrc = "Error Occurred!!\n" + ex.toString(); remoteTroubleShootMDSrc = "An error occurred!!\n" + ex.toString();
} }
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(/\((.*?(.png)|(.jpg))\)/g, `(${rawRepoURI}${basePath}/$1)`) const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(/\((.*?(.png)|(.jpg))\)/g, `(${rawRepoURI}${basePath}/$1)`)
// Render markdown // Render markdown
@@ -333,7 +325,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
const ObjectStorageMessage = `Kindly notice: this is a pretty experimental feature, hence we have some limitations. const ObjectStorageMessage = `Kindly notice: this is a pretty experimental feature, hence we have some limitations.
- Append only architecture. It will not shrink used storage if we do not perform a rebuild. - Append only architecture. It will not shrink used storage if we do not perform a rebuild.
- A bit fragile. - A bit fragile.
- During the first synchronization, the entire history to date will be transferred. For this reason, it is preferable to do this under the WiFi network. - During the first synchronization, the entire history to date will be transferred. For this reason, it is preferable to do this while connected to a Wi-Fi network.
- From the second, we always transfer only differences. - From the second, we always transfer only differences.
However, your report is needed to stabilise this. I appreciate you for your great dedication. However, your report is needed to stabilise this. I appreciate you for your great dedication.
@@ -403,7 +395,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
}) })
); );
new Setting(containerRemoteDatabaseEl) new Setting(containerRemoteDatabaseEl)
.setName("Apply Setting") .setName("Apply Settings")
.setClass("wizardHidden") .setClass("wizardHidden")
.addButton((button) => .addButton((button) =>
button button
@@ -526,7 +518,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
); );
new Setting(containerRemoteDatabaseEl) new Setting(containerRemoteDatabaseEl)
.setName("Check and Fix database configuration") .setName("Check and fix database configuration")
.setDesc("Check the database configuration, and fix if there are any problems.") .setDesc("Check the database configuration, and fix if there are any problems.")
.addButton((button) => .addButton((button) =>
button button
@@ -592,13 +584,13 @@ However, your report is needed to stabilise this. I appreciate you for your grea
} }
// HTTP user-authorization check // HTTP user-authorization check
if (responseConfig?.chttpd?.require_valid_user != "true") { if (responseConfig?.chttpd?.require_valid_user != "true") {
addResult("❗ chttpd.require_valid_user looks like wrong."); addResult("❗ chttpd.require_valid_user is wrong.");
addConfigFixButton("Set chttpd.require_valid_user = true", "chttpd/require_valid_user", "true"); addConfigFixButton("Set chttpd.require_valid_user = true", "chttpd/require_valid_user", "true");
} else { } else {
addResult("✔ chttpd.require_valid_user is ok."); addResult("✔ chttpd.require_valid_user is ok.");
} }
if (responseConfig?.chttpd_auth?.require_valid_user != "true") { if (responseConfig?.chttpd_auth?.require_valid_user != "true") {
addResult("❗ chttpd_auth.require_valid_user looks like wrong."); addResult("❗ chttpd_auth.require_valid_user is wrong.");
addConfigFixButton("Set chttpd_auth.require_valid_user = true", "chttpd_auth/require_valid_user", "true"); addConfigFixButton("Set chttpd_auth.require_valid_user = true", "chttpd_auth/require_valid_user", "true");
} else { } else {
addResult("✔ chttpd_auth.require_valid_user is ok."); addResult("✔ chttpd_auth.require_valid_user is ok.");
@@ -665,9 +657,9 @@ However, your report is needed to stabilise this. I appreciate you for your grea
})); }));
addResult(`Origin check:${org}`); addResult(`Origin check:${org}`);
if (responseHeaders["access-control-allow-credentials"] != "true") { if (responseHeaders["access-control-allow-credentials"] != "true") {
addResult("❗ CORS is not allowing credential"); addResult("❗ CORS is not allowing credentials");
} else { } else {
addResult("✔ CORS credential OK"); addResult("✔ CORS credentials OK");
} }
if (responseHeaders["access-control-allow-origin"] != org) { if (responseHeaders["access-control-allow-origin"] != org) {
addResult(`❗ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}`); addResult(`❗ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}`);
@@ -676,7 +668,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
} }
} }
addResult("--Done--", ["ob-btn-config-head"]); 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"]); 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"]);
Logger(`Checking configuration done`, LOG_LEVEL_INFO); Logger(`Checking configuration done`, LOG_LEVEL_INFO);
} catch (ex: any) { } catch (ex: any) {
if (ex?.status == 401) { if (ex?.status == 401) {
@@ -698,7 +690,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
containerRemoteDatabaseEl.createEl("h4", { text: "Effective Storage Using" }).addClass("wizardHidden") containerRemoteDatabaseEl.createEl("h4", { text: "Effective Storage Using" }).addClass("wizardHidden")
new Setting(containerRemoteDatabaseEl) new Setting(containerRemoteDatabaseEl)
.setName("Incubate Chunks in Document") .setName(confName("useEden"))
.setDesc("If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.") .setDesc("If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.useEden).onChange(async (value) => { toggle.setValue(this.plugin.settings.useEden).onChange(async (value) => {
@@ -762,7 +754,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
.setClass("wizardHidden"); .setClass("wizardHidden");
} }
new Setting(containerRemoteDatabaseEl) new Setting(containerRemoteDatabaseEl)
.setName("Data Compression (Experimental)") .setName(confName("enableCompression"))
.setDesc("Compresses data during transfer, saving space in the remote database. Note: Please ensure that all devices have v0.22.18 and connected tools are also supported compression.") .setDesc("Compresses data during transfer, saving space in the remote database. Note: Please ensure that all devices have v0.22.18 and connected tools are also supported compression.")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enableCompression).onChange(async (value) => { toggle.setValue(this.plugin.settings.enableCompression).onChange(async (value) => {
@@ -778,7 +770,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
containerRemoteDatabaseEl.createEl("h4", { text: "Confidentiality" }); containerRemoteDatabaseEl.createEl("h4", { text: "Confidentiality" });
const e2e = new Setting(containerRemoteDatabaseEl) const e2e = new Setting(containerRemoteDatabaseEl)
.setName("End to End Encryption") .setName(confName("encrypt"))
.setDesc("Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommend.") .setDesc("Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommend.")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(encrypt).onChange(async (value) => { toggle.setValue(encrypt).onChange(async (value) => {
@@ -827,7 +819,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
// if (showEncryptOptionDetail) { // if (showEncryptOptionDetail) {
const passphraseSetting = new Setting(containerRemoteDatabaseEl) const passphraseSetting = new Setting(containerRemoteDatabaseEl)
.setName("Passphrase") .setName("Passphrase")
.setDesc("Encrypting passphrase. If you change the passphrase of a existing database, overwriting the remote database is strongly recommended.") .setDesc("Encrypting passphrase. If you change the passphrase of an existing database, overwriting the remote database is strongly recommended.")
.addText((text) => { .addText((text) => {
text.setPlaceholder("") text.setPlaceholder("")
.setValue(passphrase) .setValue(passphrase)
@@ -846,7 +838,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
}); });
const usePathObfuscationEl = new Setting(containerRemoteDatabaseEl) const usePathObfuscationEl = new Setting(containerRemoteDatabaseEl)
.setName("Path Obfuscation") .setName(confName("usePathObfuscation"))
.setDesc("Obfuscate paths of files. If we configured, we should rebuild the database.") .setDesc("Obfuscate paths of files. If we configured, we should rebuild the database.")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(usePathObfuscation).onChange(async (value) => { toggle.setValue(usePathObfuscation).onChange(async (value) => {
@@ -863,7 +855,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
); );
const dynamicIteration = new Setting(containerRemoteDatabaseEl) const dynamicIteration = new Setting(containerRemoteDatabaseEl)
.setName("Use dynamic iteration count (experimental)") .setName(confName("useDynamicIterationCount"))
.setDesc("Balancing the encryption/decryption load against the length of the passphrase if toggled.") .setDesc("Balancing the encryption/decryption load against the length of the passphrase if toggled.")
.addToggle((toggle) => { .addToggle((toggle) => {
toggle.setValue(useDynamicIterationCount) toggle.setValue(useDynamicIterationCount)
@@ -896,7 +888,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
) )
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Apply and Fetch") .setButtonText("Apply and fetch")
.setWarning() .setWarning()
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
@@ -905,7 +897,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
) )
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Apply and Rebuild") .setButtonText("Apply and rebuild")
.setWarning() .setWarning()
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
@@ -946,7 +938,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
return; return;
} }
if (encrypt && !(await testCrypt())) { if (encrypt && !(await testCrypt())) {
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL_NOTICE); Logger("WARNING! Your device does not support encryption.", LOG_LEVEL_NOTICE);
return; return;
} }
if (!(await checkWorkingPassphrase()) && !sendToServer) { if (!(await checkWorkingPassphrase()) && !sendToServer) {
@@ -978,7 +970,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
return; return;
} }
if (encrypt && !(await testCrypt())) { if (encrypt && !(await testCrypt())) {
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL_NOTICE); Logger("WARNING! Your device does not support encryption.", LOG_LEVEL_NOTICE);
return; return;
} }
if (!encrypt) { if (!encrypt) {
@@ -991,7 +983,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount; this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount;
this.plugin.settings.usePathObfuscation = usePathObfuscation; this.plugin.settings.usePathObfuscation = usePathObfuscation;
this.plugin.settings.isConfigured = true; this.plugin.settings.isConfigured = true;
Logger("All synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) Logger("All synchronizations have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE)
await this.plugin.saveSettings(); await this.plugin.saveSettings();
updateE2EControls(); updateE2EControls();
applyDisplayEnabled(); applyDisplayEnabled();
@@ -1127,7 +1119,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
let buttonApplyFilename: ButtonComponent; let buttonApplyFilename: ButtonComponent;
new Setting(containerGeneralSettingsEl) new Setting(containerGeneralSettingsEl)
.setName("Filename") .setName("Filename")
.setDesc("If you set this, all settings are saved in a markdown file. You will also be notified when new settings were arrived. You can set different files by the platform.") .setDesc("If you set this, all settings are saved in a markdown file. You will be notified when new settings arrive. You can set different files by the platform.")
.addText((text) => { .addText((text) => {
text.setPlaceholder("livesync/setting.md") text.setPlaceholder("livesync/setting.md")
.setValue(settingSyncFile) .setValue(settingSyncFile)
@@ -1233,7 +1225,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
let currentPreset = "NONE"; let currentPreset = "NONE";
containerSyncSettingEl.createEl("div", containerSyncSettingEl.createEl("div",
{ text: `Please select any preset to complete wizard.` } { text: `Please select any preset to complete the wizard.` }
).addClasses(["op-warn-info", "wizardOnly"]); ).addClasses(["op-warn-info", "wizardOnly"]);
const options: Record<string, string> = this.plugin.settings.remoteType == REMOTE_COUCHDB ? { const options: Record<string, string> = this.plugin.settings.remoteType == REMOTE_COUCHDB ? {
NONE: "", NONE: "",
@@ -1298,7 +1290,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
} }
Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL_NOTICE); Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL_NOTICE);
} else { } else {
Logger("All synchronization disabled.", LOG_LEVEL_NOTICE); Logger("All synchronizations disabled.", LOG_LEVEL_NOTICE);
this.plugin.settings = { this.plugin.settings = {
...this.plugin.settings, ...this.plugin.settings,
...presetAllDisabled ...presetAllDisabled
@@ -1316,7 +1308,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
Logger("All done! Please set up subsequent devices with 'Copy current settings as a new setup URI' and 'Use the copied setup URI'.", LOG_LEVEL_NOTICE); Logger("All done! Please set up subsequent devices with 'Copy current settings as a new setup URI' and 'Use the copied setup URI'.", LOG_LEVEL_NOTICE);
await this.plugin.addOnSetup.command_copySetupURI(); await this.plugin.addOnSetup.command_copySetupURI();
} else { } else {
this.askReload(); this.plugin.askReload();
} }
} }
}) })
@@ -1382,7 +1374,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Sync on Save") .setName("Sync on Save")
.setDesc("When you save file, sync automatically") .setDesc("When you save a file, sync automatically")
.setClass("wizardHidden") .setClass("wizardHidden")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.syncOnSave).onChange(async (value) => { toggle.setValue(this.plugin.settings.syncOnSave).onChange(async (value) => {
@@ -1393,7 +1385,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
) )
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Sync on Editor Save") .setName("Sync on Editor Save")
.setDesc("When you save file on the editor, sync automatically") .setDesc("When you save a file in the editor, sync automatically")
.setClass("wizardHidden") .setClass("wizardHidden")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.syncOnEditorSave).onChange(async (value) => { toggle.setValue(this.plugin.settings.syncOnEditorSave).onChange(async (value) => {
@@ -1404,7 +1396,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
) )
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Sync on File Open") .setName("Sync on File Open")
.setDesc("When you open file, sync automatically") .setDesc("When you open a file, sync automatically")
.setClass("wizardHidden") .setClass("wizardHidden")
.addToggle((toggle) => .addToggle((toggle) =>
toggle.setValue(this.plugin.settings.syncOnFileOpen).onChange(async (value) => { toggle.setValue(this.plugin.settings.syncOnFileOpen).onChange(async (value) => {
@@ -1492,7 +1484,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
); );
containerSyncSettingEl.createEl("h4", { text: "Compatibility" }).addClass("wizardHidden"); containerSyncSettingEl.createEl("h4", { text: "Compatibility" }).addClass("wizardHidden");
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Always resolve conflict manually") .setName("Always resolve conflicts manually")
.setDesc("If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)") .setDesc("If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)")
.setClass("wizardHidden") .setClass("wizardHidden")
.addToggle((toggle) => .addToggle((toggle) =>
@@ -1650,7 +1642,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
); );
new Setting(containerSyncSettingEl) new Setting(containerSyncSettingEl)
.setName("Enhance chunk size") .setName(confName("customChunkSize"))
.setDesc("Enhance chunk size for binary files (Ratio). This cannot be increased when using IBM Cloudant.") .setDesc("Enhance chunk size for binary files (Ratio). This cannot be increased when using IBM Cloudant.")
.setClass("wizardHidden") .setClass("wizardHidden")
.addText((text) => { .addText((text) => {
@@ -1689,7 +1681,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
const syncFilesSetting = new Setting(containerSyncSettingEl) const syncFilesSetting = new Setting(containerSyncSettingEl)
.setName("Synchronising files") .setName("Synchronising files")
.setDesc("(RegExp) Empty to sync all files. set filter as a regular expression to limit synchronising files.") .setDesc("(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.")
.setClass("wizardHidden") .setClass("wizardHidden")
new MultipleRegExpControl( new MultipleRegExpControl(
{ {
@@ -1897,7 +1889,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea
responseConfig["admins"] = REDACTED; responseConfig["admins"] = REDACTED;
} catch (ex) { } catch (ex) {
responseConfig = "Requesting information to the remote CouchDB has been failed. If you are using IBM Cloudant, it is the normal behaviour." responseConfig = "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour."
} }
} else if (this.plugin.settings.remoteType == REMOTE_MINIO) { } else if (this.plugin.settings.remoteType == REMOTE_MINIO) {
responseConfig = "Object Storage Synchronisation"; responseConfig = "Object Storage Synchronisation";
@@ -1940,7 +1932,7 @@ ${stringifyYaml(pluginConfig)}`;
if (this.plugin.replicator.remoteLockedAndDeviceNotAccepted) { if (this.plugin.replicator.remoteLockedAndDeviceNotAccepted) {
const c = containerHatchEl.createEl("div", { const c = containerHatchEl.createEl("div", {
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. ", text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. It caused by some operations like this. Re-initialized. Local database initialization should be required. Please back your vault up, reset the local database, and press 'Mark this device as resolved'. ",
}); });
c.createEl("button", { text: "I'm ready, mark this device 'resolved'" }, (e) => { c.createEl("button", { text: "I'm ready, mark this device 'resolved'" }, (e) => {
e.addClass("mod-warning"); e.addClass("mod-warning");
@@ -1975,7 +1967,7 @@ ${stringifyYaml(pluginConfig)}`;
.onClick(async () => { .onClick(async () => {
this.plugin.settings.isConfigured = false; this.plugin.settings.isConfigured = false;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
this.askReload(); this.plugin.askReload();
})); }));
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).` }); 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"); hatchWarn.addClass("op-warn-info");
@@ -2027,7 +2019,7 @@ ${stringifyYaml(pluginConfig)}`;
} }
new Setting(containerHatchEl) new Setting(containerHatchEl)
.setName("Verify and repair all files") .setName("Verify and repair all files")
.setDesc("Compare the content of files between on local database and storage. If not matched, you will asked which one want to keep.") .setDesc("Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.")
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Verify all") .setButtonText("Verify all")
@@ -2130,7 +2122,7 @@ ${stringifyYaml(pluginConfig)}`;
} }
} }
} else { } else {
Logger(`Something went wrong on converting ${docName}`, LOG_LEVEL_NOTICE); Logger(`Something went wrong while converting ${docName}`, LOG_LEVEL_NOTICE);
Logger(ex, LOG_LEVEL_VERBOSE); Logger(ex, LOG_LEVEL_VERBOSE);
// Something wrong. // Something wrong.
} }
@@ -2166,7 +2158,7 @@ ${stringifyYaml(pluginConfig)}`;
toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => { toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => {
this.plugin.settings.suspendFileWatching = value; this.plugin.settings.suspendFileWatching = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
this.askReload(); this.plugin.askReload();
}) })
); );
new Setting(containerHatchEl) new Setting(containerHatchEl)
@@ -2176,7 +2168,7 @@ ${stringifyYaml(pluginConfig)}`;
toggle.setValue(this.plugin.settings.suspendParseReplicationResult).onChange(async (value) => { toggle.setValue(this.plugin.settings.suspendParseReplicationResult).onChange(async (value) => {
this.plugin.settings.suspendParseReplicationResult = value; this.plugin.settings.suspendParseReplicationResult = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
this.askReload(); this.plugin.askReload();
}) })
); );
new Setting(containerHatchEl) new Setting(containerHatchEl)
@@ -2284,7 +2276,7 @@ ${stringifyYaml(pluginConfig)}`;
}) })
new Setting(containerHatchEl) new Setting(containerHatchEl)
.setName("The Hash algorithm for chunk IDs") .setName(confName("hashAlg"))
.setDesc("xxhash64 is the current default.") .setDesc("xxhash64 is the current default.")
.setClass("wizardHidden") .setClass("wizardHidden")
.addDropdown((dropdown) => .addDropdown((dropdown) =>
@@ -2313,6 +2305,15 @@ ${stringifyYaml(pluginConfig)}`;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerHatchEl)
.setName("Do not check configuration mismatch before replication")
.setDesc("")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.disableCheckingConfigMismatch).onChange(async (value) => {
this.plugin.settings.disableCheckingConfigMismatch = value;
await this.plugin.saveSettings();
})
);
addScreenElement("50", containerHatchEl); addScreenElement("50", containerHatchEl);
@@ -2409,6 +2410,26 @@ ${stringifyYaml(pluginConfig)}`;
containerMaintenanceEl.createEl("h4", { text: "Remote" }); containerMaintenanceEl.createEl("h4", { text: "Remote" });
if (this.plugin.settings.remoteType == REMOTE_COUCHDB) {
new Setting(containerMaintenanceEl)
.setName("Perform compaction")
.setDesc("Compaction discards all of Eden in the non-latest revisions, reducing the storage usage. However, this operation requires the same free space on the remote as the current database.")
.addButton((button) =>
button
.setButtonText("Perform")
.setDisabled(false)
.onClick(async () => {
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
Logger(`Compaction has been began`, LOG_LEVEL_NOTICE, "compaction")
if (await replicator.compactRemote(this.plugin.settings)) {
Logger(`Compaction has been completed!`, LOG_LEVEL_NOTICE, "compaction");
} else {
Logger(`Compaction has been failed!`, LOG_LEVEL_NOTICE, "compaction");
}
})
)
}
new Setting(containerMaintenanceEl) new Setting(containerMaintenanceEl)
.setName("Lock remote") .setName("Lock remote")
.setDesc("Lock remote to prevent synchronization with other devices.") .setDesc("Lock remote to prevent synchronization with other devices.")
@@ -2435,6 +2456,7 @@ ${stringifyYaml(pluginConfig)}`;
}) })
) )
if (this.plugin.settings.remoteType != REMOTE_COUCHDB) { if (this.plugin.settings.remoteType != REMOTE_COUCHDB) {
new Setting(containerMaintenanceEl) new Setting(containerMaintenanceEl)
.setName("Reset journal received history") .setName("Reset journal received history")

View File

@@ -18,6 +18,29 @@ I have a lot of respect for that plugin, even though it is sometimes treated as
Hooray for open source, and generous licences, and the sharing of knowledge by experts. Hooray for open source, and generous licences, and the sharing of knowledge by experts.
#### Version history #### Version history
- 0.23.7
- Fixed:
- No longer missing tasks which have queued as the same key (e.g., for the same operation to the same file).
- This occurs, for example, with hidden files that have been changed multiple times in a very short period of time, such as `appearance.json`. Thanks for the report!
- Some trivial issues have been fixed.
- New feature:
- Reloading Obsidian can be scheduled until that file and database operations are stable.
- 0.23.6:
- Fixed:
- Now the remote chunks could be decrypted even if we are using `Incubate chunks in Document`. (The note of 0.23.6 has been fixed).
- Chunk retrieving with `Incubate chunks in document` got more efficiently.
- No longer task processor misses the completed tasks.
- Replication is no longer started automatically during changes in window visibility (e.g., task switching on the desktop) when off-focused.
- 0.23.5:
- New feature:
- Now we can check configuration mismatching between clients before synchronisation.
- Default: enabled / Preferred: enabled / We can disable this by the `Do not check configuration mismatch before replication` toggle in the `Hatch` pane.
- It detects configuration mismatches and prevents synchronisation failures and wasted storage.
- Now we can perform remote database compaction from the `Maintenance` pane.
- Fixed:
- We can detect the bucket could not be reachable.
- Note:
- Known inexplicable behaviour: Recently, (Maybe while enabling `Incubate chunks in Document` and `Fetch chunks on demand` or some more toggles), our customisation sync data is sometimes corrupted. It will be addressed by the next release.
- 0.23.4 - 0.23.4
- Fixed: - Fixed:
- No longer experimental configuration is shown on the Minimal Setup. - No longer experimental configuration is shown on the Minimal Setup.