mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-18 13:31:17 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6bf453a6d | ||
|
|
e80cdc2dae | ||
|
|
60780678fd | ||
|
|
273e7a2b63 | ||
|
|
02673a1631 |
@@ -60,7 +60,7 @@ RUN apt-get update \
|
|||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# Install workspace dependencies first (layer-cache friendly)
|
# Install workspace dependencies first (layer-cache friendly)
|
||||||
COPY package.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Copy the full source tree and build the CLI bundle
|
# Copy the full source tree and build the CLI bundle
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
|
|
||||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
|
||||||
import { AbstractModule } from "../AbstractModule";
|
|
||||||
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
|
||||||
import type { LiveSyncCore } from "../../main";
|
|
||||||
|
|
||||||
// Note:
|
|
||||||
// This module registers only the `getNewReplicator` handler for the P2P replicator.
|
|
||||||
// `useP2PReplicator` (see P2PReplicatorCore.ts) already registers the same `getNewReplicator`
|
|
||||||
// handler internally, so this module is redundant in environments that call `useP2PReplicator`.
|
|
||||||
// Register this module only in environments that do NOT use `useP2PReplicator` (e.g. CLI).
|
|
||||||
// In other words: just resolving `getNewReplicator` via this module is all that is needed
|
|
||||||
// to satisfy what `useP2PReplicator` requires from the replicator service.
|
|
||||||
export class ModuleReplicatorP2P extends AbstractModule {
|
|
||||||
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
|
|
||||||
const settings = { ...this.settings, ...settingOverride };
|
|
||||||
if (settings.remoteType == REMOTE_P2P) {
|
|
||||||
return Promise.resolve(new LiveSyncTrysteroReplicator(this.core));
|
|
||||||
}
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
|
||||||
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
|
||||||
import { normalizePath } from "../../deps.ts";
|
|
||||||
import {
|
|
||||||
FlagFilesHumanReadable,
|
|
||||||
FlagFilesOriginal,
|
|
||||||
REMOTE_MINIO,
|
|
||||||
TweakValuesShouldMatchedTemplate,
|
|
||||||
type ObsidianLiveSyncSettings,
|
|
||||||
} from "../../lib/src/common/types.ts";
|
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
|
||||||
import type { LiveSyncCore } from "../../main.ts";
|
|
||||||
import FetchEverything from "../features/SetupWizard/dialogs/FetchEverything.svelte";
|
|
||||||
import RebuildEverything from "../features/SetupWizard/dialogs/RebuildEverything.svelte";
|
|
||||||
import { extractObject } from "octagonal-wheels/object";
|
|
||||||
import { SvelteDialogManagerBase } from "@lib/UI/svelteDialog.ts";
|
|
||||||
import type { ServiceContext } from "@lib/services/base/ServiceBase.ts";
|
|
||||||
|
|
||||||
export class ModuleRedFlag extends AbstractModule {
|
|
||||||
async isFlagFileExist(path: string) {
|
|
||||||
const redflag = await this.core.storageAccess.isExists(normalizePath(path));
|
|
||||||
if (redflag) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteFlagFile(path: string) {
|
|
||||||
try {
|
|
||||||
const isFlagged = await this.core.storageAccess.isExists(normalizePath(path));
|
|
||||||
if (isFlagged) {
|
|
||||||
await this.core.storageAccess.delete(path, true);
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
this._log(`Could not delete ${path}`);
|
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isSuspendFlagActive = async () => await this.isFlagFileExist(FlagFilesOriginal.SUSPEND_ALL);
|
|
||||||
isRebuildFlagActive = async () =>
|
|
||||||
(await this.isFlagFileExist(FlagFilesOriginal.REBUILD_ALL)) ||
|
|
||||||
(await this.isFlagFileExist(FlagFilesHumanReadable.REBUILD_ALL));
|
|
||||||
isFetchAllFlagActive = async () =>
|
|
||||||
(await this.isFlagFileExist(FlagFilesOriginal.FETCH_ALL)) ||
|
|
||||||
(await this.isFlagFileExist(FlagFilesHumanReadable.FETCH_ALL));
|
|
||||||
|
|
||||||
async cleanupRebuildFlag() {
|
|
||||||
await this.deleteFlagFile(FlagFilesOriginal.REBUILD_ALL);
|
|
||||||
await this.deleteFlagFile(FlagFilesHumanReadable.REBUILD_ALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanupFetchAllFlag() {
|
|
||||||
await this.deleteFlagFile(FlagFilesOriginal.FETCH_ALL);
|
|
||||||
await this.deleteFlagFile(FlagFilesHumanReadable.FETCH_ALL);
|
|
||||||
}
|
|
||||||
// dialogManager = new SvelteDialogManagerBase(this.core);
|
|
||||||
get dialogManager(): SvelteDialogManagerBase<ServiceContext> {
|
|
||||||
return this.core.services.UI.dialogManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust setting to remote if needed.
|
|
||||||
* @param extra result of dialogues that may contain preventFetchingConfig flag (e.g, from FetchEverything or RebuildEverything)
|
|
||||||
* @param config current configuration to retrieve remote preferred config
|
|
||||||
*/
|
|
||||||
async adjustSettingToRemoteIfNeeded(extra: { preventFetchingConfig: boolean }, config: ObsidianLiveSyncSettings) {
|
|
||||||
if (extra && extra.preventFetchingConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote configuration fetched and applied.
|
|
||||||
if (await this.adjustSettingToRemote(config)) {
|
|
||||||
config = this.core.settings;
|
|
||||||
} else {
|
|
||||||
this._log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
|
|
||||||
}
|
|
||||||
console.debug(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust setting to remote configuration.
|
|
||||||
* @param config current configuration to retrieve remote preferred config
|
|
||||||
* @returns updated configuration if applied, otherwise null.
|
|
||||||
*/
|
|
||||||
async adjustSettingToRemote(config: ObsidianLiveSyncSettings) {
|
|
||||||
// Fetch remote configuration unless prevented.
|
|
||||||
const SKIP_FETCH = "Skip and proceed";
|
|
||||||
const RETRY_FETCH = "Retry (recommended)";
|
|
||||||
let canProceed = false;
|
|
||||||
do {
|
|
||||||
const remoteTweaks = await this.services.tweakValue.fetchRemotePreferred(config);
|
|
||||||
if (!remoteTweaks) {
|
|
||||||
const choice = await this.core.confirm.askSelectStringDialogue(
|
|
||||||
"Could not fetch configuration from remote. If you are new to the Self-hosted LiveSync, this might be expected. If not, you should check your network or server settings.",
|
|
||||||
[SKIP_FETCH, RETRY_FETCH] as const,
|
|
||||||
{
|
|
||||||
defaultAction: RETRY_FETCH,
|
|
||||||
timeout: 0,
|
|
||||||
title: "Fetch Remote Configuration Failed",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (choice === SKIP_FETCH) {
|
|
||||||
canProceed = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const necessary = extractObject(TweakValuesShouldMatchedTemplate, remoteTweaks);
|
|
||||||
// Check if any necessary tweak value is different from current config.
|
|
||||||
const differentItems = Object.entries(necessary).filter(([key, value]) => {
|
|
||||||
return (config as any)[key] !== value;
|
|
||||||
});
|
|
||||||
if (differentItems.length === 0) {
|
|
||||||
this._log(
|
|
||||||
"Remote configuration matches local configuration. No changes applied.",
|
|
||||||
LOG_LEVEL_NOTICE
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await this.core.confirm.askSelectStringDialogue(
|
|
||||||
"Your settings differed slightly from the server's. The plug-in has supplemented the incompatible parts with the server settings!",
|
|
||||||
["OK"] as const,
|
|
||||||
{
|
|
||||||
defaultAction: "OK",
|
|
||||||
timeout: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
config = {
|
|
||||||
...config,
|
|
||||||
...Object.fromEntries(differentItems),
|
|
||||||
} satisfies ObsidianLiveSyncSettings;
|
|
||||||
this.core.settings = config;
|
|
||||||
await this.core.services.setting.saveSettingData();
|
|
||||||
this._log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
|
||||||
canProceed = true;
|
|
||||||
return this.core.settings;
|
|
||||||
}
|
|
||||||
} while (!canProceed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process vault initialisation with suspending file watching and sync.
|
|
||||||
* @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process.
|
|
||||||
* @param keepSuspending whether to keep suspending file watching after the process.
|
|
||||||
* @returns result of the process, or false if error occurs.
|
|
||||||
*/
|
|
||||||
async processVaultInitialisation(proc: () => Promise<boolean>, keepSuspending = false) {
|
|
||||||
try {
|
|
||||||
// Disable batch saving and file watching during initialisation.
|
|
||||||
this.settings.batchSave = false;
|
|
||||||
await this.services.setting.suspendAllSync();
|
|
||||||
await this.services.setting.suspendExtraSync();
|
|
||||||
this.settings.suspendFileWatching = true;
|
|
||||||
await this.saveSettings();
|
|
||||||
try {
|
|
||||||
const result = await proc();
|
|
||||||
return result;
|
|
||||||
} catch (ex) {
|
|
||||||
this._log("Error during vault initialisation process.", LOG_LEVEL_NOTICE);
|
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
this._log("Error during vault initialisation.", LOG_LEVEL_NOTICE);
|
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
if (!keepSuspending) {
|
|
||||||
// Re-enable file watching after initialisation.
|
|
||||||
this.settings.suspendFileWatching = false;
|
|
||||||
await this.saveSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the rebuild everything scheduled operation.
|
|
||||||
* @returns true if can be continued, false if app restart is needed.
|
|
||||||
*/
|
|
||||||
async onRebuildEverythingScheduled() {
|
|
||||||
const method = await this.dialogManager.openWithExplicitCancel(RebuildEverything);
|
|
||||||
if (method === "cancelled") {
|
|
||||||
// Clean up the flag file and restart the app.
|
|
||||||
this._log("Rebuild everything cancelled by user.", LOG_LEVEL_NOTICE);
|
|
||||||
await this.cleanupRebuildFlag();
|
|
||||||
this.services.appLifecycle.performRestart();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const { extra } = method;
|
|
||||||
await this.adjustSettingToRemoteIfNeeded(extra, this.settings);
|
|
||||||
return await this.processVaultInitialisation(async () => {
|
|
||||||
await this.core.rebuilder.$rebuildEverything();
|
|
||||||
await this.cleanupRebuildFlag();
|
|
||||||
this._log("Rebuild everything operation completed.", LOG_LEVEL_NOTICE);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handle the fetch all scheduled operation.
|
|
||||||
* @returns true if can be continued, false if app restart is needed.
|
|
||||||
*/
|
|
||||||
async onFetchAllScheduled() {
|
|
||||||
const method = await this.dialogManager.openWithExplicitCancel(FetchEverything);
|
|
||||||
if (method === "cancelled") {
|
|
||||||
this._log("Fetch everything cancelled by user.", LOG_LEVEL_NOTICE);
|
|
||||||
// Clean up the flag file and restart the app.
|
|
||||||
await this.cleanupFetchAllFlag();
|
|
||||||
this.services.appLifecycle.performRestart();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const { vault, extra } = method;
|
|
||||||
// If remote is MinIO, makeLocalChunkBeforeSync is not available. (because no-deduplication on sending).
|
|
||||||
const makeLocalChunkBeforeSyncAvailable = this.settings.remoteType !== REMOTE_MINIO;
|
|
||||||
const mapVaultStateToAction = {
|
|
||||||
identical: {
|
|
||||||
// If both are identical, no need to make local files/chunks before sync,
|
|
||||||
// Just for the efficiency, chunks should be made before sync.
|
|
||||||
makeLocalChunkBeforeSync: makeLocalChunkBeforeSyncAvailable,
|
|
||||||
makeLocalFilesBeforeSync: false,
|
|
||||||
},
|
|
||||||
independent: {
|
|
||||||
// If both are independent, nothing needs to be made before sync.
|
|
||||||
// Respect the remote state.
|
|
||||||
makeLocalChunkBeforeSync: false,
|
|
||||||
makeLocalFilesBeforeSync: false,
|
|
||||||
},
|
|
||||||
unbalanced: {
|
|
||||||
// If both are unbalanced, local files should be made before sync to avoid data loss.
|
|
||||||
// Then, chunks should be made before sync for the efficiency, but also the metadata made and should be detected as conflicting.
|
|
||||||
makeLocalChunkBeforeSync: false,
|
|
||||||
makeLocalFilesBeforeSync: true,
|
|
||||||
},
|
|
||||||
cancelled: {
|
|
||||||
// Cancelled case, not actually used.
|
|
||||||
makeLocalChunkBeforeSync: false,
|
|
||||||
makeLocalFilesBeforeSync: false,
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
return await this.processVaultInitialisation(async () => {
|
|
||||||
await this.adjustSettingToRemoteIfNeeded(extra, this.settings);
|
|
||||||
// Okay, proceed to fetch everything.
|
|
||||||
const { makeLocalChunkBeforeSync, makeLocalFilesBeforeSync } = mapVaultStateToAction[vault];
|
|
||||||
this._log(
|
|
||||||
`Fetching everything with settings: makeLocalChunkBeforeSync=${makeLocalChunkBeforeSync}, makeLocalFilesBeforeSync=${makeLocalFilesBeforeSync}`,
|
|
||||||
LOG_LEVEL_INFO
|
|
||||||
);
|
|
||||||
await this.core.rebuilder.$fetchLocal(makeLocalChunkBeforeSync, !makeLocalFilesBeforeSync);
|
|
||||||
await this.cleanupFetchAllFlag();
|
|
||||||
this._log("Fetch everything operation completed. Vault files will be gradually synced.", LOG_LEVEL_NOTICE);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onSuspendAllScheduled() {
|
|
||||||
this._log("SCRAM is detected. All operations are suspended.", LOG_LEVEL_NOTICE);
|
|
||||||
return await this.processVaultInitialisation(async () => {
|
|
||||||
this._log(
|
|
||||||
"All operations are suspended as per SCRAM.\nLogs will be written to the file. This might be a performance impact.",
|
|
||||||
LOG_LEVEL_NOTICE
|
|
||||||
);
|
|
||||||
this.settings.writeLogToTheFile = true;
|
|
||||||
await this.core.services.setting.saveSettingData();
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyAndUnlockSuspension() {
|
|
||||||
if (!this.settings.suspendFileWatching) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(await this.core.confirm.askYesNoDialog(
|
|
||||||
"Do you want to resume file and database processing, and restart obsidian now?",
|
|
||||||
{ defaultOption: "Yes", timeout: 15 }
|
|
||||||
)) != "yes"
|
|
||||||
) {
|
|
||||||
// TODO: Confirm actually proceed to next process.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
this.settings.suspendFileWatching = false;
|
|
||||||
await this.saveSettings();
|
|
||||||
this.services.appLifecycle.performRestart();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processFlagFilesOnStartup(): Promise<boolean> {
|
|
||||||
const isFlagSuspensionActive = await this.isSuspendFlagActive();
|
|
||||||
const isFlagRebuildActive = await this.isRebuildFlagActive();
|
|
||||||
const isFlagFetchAllActive = await this.isFetchAllFlagActive();
|
|
||||||
// TODO: Address the case when both flags are active (very unlikely though).
|
|
||||||
// if(isFlagFetchAllActive && isFlagRebuildActive) {
|
|
||||||
// const message = "Rebuild everything and Fetch everything flags are both detected.";
|
|
||||||
// await this.core.confirm.askSelectStringDialogue(
|
|
||||||
// "Both Rebuild Everything and Fetch Everything flags are detected. Please remove one of them and restart the app.",
|
|
||||||
// ["OK"] as const,)
|
|
||||||
if (isFlagFetchAllActive) {
|
|
||||||
const res = await this.onFetchAllScheduled();
|
|
||||||
if (res) {
|
|
||||||
return await this.verifyAndUnlockSuspension();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isFlagRebuildActive) {
|
|
||||||
const res = await this.onRebuildEverythingScheduled();
|
|
||||||
if (res) {
|
|
||||||
return await this.verifyAndUnlockSuspension();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isFlagSuspensionActive) {
|
|
||||||
const res = await this.onSuspendAllScheduled();
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _everyOnLayoutReady(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const flagProcessResult = await this.processFlagFilesOnStartup();
|
|
||||||
return flagProcessResult;
|
|
||||||
} catch (ex) {
|
|
||||||
this._log("Something went wrong on FlagFile Handling", LOG_LEVEL_NOTICE);
|
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
|
||||||
super.onBindFunction(core, services);
|
|
||||||
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,429 +0,0 @@
|
|||||||
import { unique } from "octagonal-wheels/collection";
|
|
||||||
import { throttle } from "octagonal-wheels/function";
|
|
||||||
import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "../../common/events.ts";
|
|
||||||
import { BASE_IS_NEW, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
|
|
||||||
import {
|
|
||||||
type FilePathWithPrefixLC,
|
|
||||||
type FilePathWithPrefix,
|
|
||||||
type MetaEntry,
|
|
||||||
isMetaEntry,
|
|
||||||
type EntryDoc,
|
|
||||||
LOG_LEVEL_VERBOSE,
|
|
||||||
LOG_LEVEL_NOTICE,
|
|
||||||
LOG_LEVEL_INFO,
|
|
||||||
LOG_LEVEL_DEBUG,
|
|
||||||
type UXFileInfoStub,
|
|
||||||
type LOG_LEVEL,
|
|
||||||
} from "../../lib/src/common/types.ts";
|
|
||||||
import { isAnyNote } from "../../lib/src/common/utils.ts";
|
|
||||||
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
|
||||||
import { withConcurrency } from "octagonal-wheels/iterable/map";
|
|
||||||
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
|
||||||
import type { LiveSyncCore } from "../../main.ts";
|
|
||||||
export class ModuleInitializerFile extends AbstractModule {
|
|
||||||
private _detectedErrors = new Set<string>();
|
|
||||||
|
|
||||||
private logDetectedError(message: string, logLevel: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) {
|
|
||||||
this._detectedErrors.add(message);
|
|
||||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
|
||||||
this._log(message, logLevel, key);
|
|
||||||
}
|
|
||||||
private resetDetectedError(message: string) {
|
|
||||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
|
||||||
this._detectedErrors.delete(message);
|
|
||||||
}
|
|
||||||
private async _performFullScan(showingNotice?: boolean, ignoreSuspending: boolean = false): Promise<boolean> {
|
|
||||||
this._log("Opening the key-value database", LOG_LEVEL_VERBOSE);
|
|
||||||
const isInitialized = (await this.core.kvDB.get<boolean>("initialized")) || false;
|
|
||||||
// synchronize all files between database and storage.
|
|
||||||
|
|
||||||
const ERR_NOT_CONFIGURED =
|
|
||||||
"LiveSync is not configured yet. Synchronising between the storage and the local database is now prevented.";
|
|
||||||
if (!this.settings.isConfigured) {
|
|
||||||
this.logDetectedError(ERR_NOT_CONFIGURED, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.resetDetectedError(ERR_NOT_CONFIGURED);
|
|
||||||
|
|
||||||
const ERR_SUSPENDING =
|
|
||||||
"Now suspending file watching. Synchronising between the storage and the local database is now prevented.";
|
|
||||||
if (!ignoreSuspending && this.settings.suspendFileWatching) {
|
|
||||||
this.logDetectedError(ERR_SUSPENDING, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const MSG_IN_REMEDIATION = `Started in remediation Mode! (Max mtime for reflect events is set). Synchronising between the storage and the local database is now prevented.`;
|
|
||||||
this.resetDetectedError(ERR_SUSPENDING);
|
|
||||||
if (this.settings.maxMTimeForReflectEvents > 0) {
|
|
||||||
this.logDetectedError(MSG_IN_REMEDIATION, LOG_LEVEL_NOTICE, "syncAll");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.resetDetectedError(MSG_IN_REMEDIATION);
|
|
||||||
|
|
||||||
if (showingNotice) {
|
|
||||||
this._log("Initializing", LOG_LEVEL_NOTICE, "syncAll");
|
|
||||||
}
|
|
||||||
if (isInitialized) {
|
|
||||||
this._log("Restoring storage state", LOG_LEVEL_VERBOSE);
|
|
||||||
await this.core.storageAccess.restoreState();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._log("Initialize and checking database files");
|
|
||||||
this._log("Checking deleted files");
|
|
||||||
await this.collectDeletedFiles();
|
|
||||||
|
|
||||||
this._log("Collecting local files on the storage", LOG_LEVEL_VERBOSE);
|
|
||||||
const filesStorageSrc = await this.core.storageAccess.getFiles();
|
|
||||||
|
|
||||||
const _filesStorage = [] as typeof filesStorageSrc;
|
|
||||||
|
|
||||||
for (const f of filesStorageSrc) {
|
|
||||||
if (await this.services.vault.isTargetFile(f.path)) {
|
|
||||||
_filesStorage.push(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const convertCase = <FilePathWithPrefix>(path: FilePathWithPrefix): FilePathWithPrefixLC => {
|
|
||||||
if (this.settings.handleFilenameCaseSensitive) {
|
|
||||||
return path as FilePathWithPrefixLC;
|
|
||||||
}
|
|
||||||
return (path as string).toLowerCase() as FilePathWithPrefixLC;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If handleFilenameCaseSensitive is enabled, `FilePathWithPrefixLC` is the same as `FilePathWithPrefix`.
|
|
||||||
|
|
||||||
const storageFileNameMap = Object.fromEntries(
|
|
||||||
_filesStorage.map((e) => [e.path, e] as [FilePathWithPrefix, UXFileInfoStub])
|
|
||||||
);
|
|
||||||
|
|
||||||
const storageFileNames = Object.keys(storageFileNameMap) as FilePathWithPrefix[];
|
|
||||||
|
|
||||||
const storageFileNameCapsPair = storageFileNames.map(
|
|
||||||
(e) => [e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC]
|
|
||||||
);
|
|
||||||
|
|
||||||
// const storageFileNameCS2CI = Object.fromEntries(storageFileNameCapsPair) as Record<FilePathWithPrefix, FilePathWithPrefixLC>;
|
|
||||||
const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map((e) => [e[1], e[0]])) as Record<
|
|
||||||
FilePathWithPrefixLC,
|
|
||||||
FilePathWithPrefix
|
|
||||||
>;
|
|
||||||
|
|
||||||
this._log("Collecting local files on the DB", LOG_LEVEL_VERBOSE);
|
|
||||||
const _DBEntries = [] as MetaEntry[];
|
|
||||||
let count = 0;
|
|
||||||
// Fetch all documents from the database (including conflicts to prevent overwriting).
|
|
||||||
for await (const doc of this.localDatabase.findAllNormalDocs({ conflicts: true })) {
|
|
||||||
count++;
|
|
||||||
if (count % 25 == 0)
|
|
||||||
this._log(
|
|
||||||
`Collecting local files on the DB: ${count}`,
|
|
||||||
showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO,
|
|
||||||
"syncAll"
|
|
||||||
);
|
|
||||||
const path = this.getPath(doc);
|
|
||||||
|
|
||||||
if (isValidPath(path) && (await this.services.vault.isTargetFile(path))) {
|
|
||||||
if (!isMetaEntry(doc)) {
|
|
||||||
this._log(`Invalid entry: ${path}`, LOG_LEVEL_INFO);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_DBEntries.push(doc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const databaseFileNameMap = Object.fromEntries(
|
|
||||||
_DBEntries.map((e) => [this.getPath(e), e] as [FilePathWithPrefix, MetaEntry])
|
|
||||||
);
|
|
||||||
const databaseFileNames = Object.keys(databaseFileNameMap) as FilePathWithPrefix[];
|
|
||||||
const databaseFileNameCapsPair = databaseFileNames.map(
|
|
||||||
(e) => [e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC]
|
|
||||||
);
|
|
||||||
// const databaseFileNameCS2CI = Object.fromEntries(databaseFileNameCapsPair) as Record<FilePathWithPrefix, FilePathWithPrefixLC>;
|
|
||||||
const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map((e) => [e[1], e[0]])) as Record<
|
|
||||||
FilePathWithPrefix,
|
|
||||||
FilePathWithPrefixLC
|
|
||||||
>;
|
|
||||||
|
|
||||||
const allFiles = unique([
|
|
||||||
...Object.keys(databaseFileNameCI2CS),
|
|
||||||
...Object.keys(storageFileNameCI2CS),
|
|
||||||
]) as FilePathWithPrefixLC[];
|
|
||||||
|
|
||||||
this._log(`Total files in the database: ${databaseFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
|
||||||
this._log(`Total files in the storage: ${storageFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
|
||||||
this._log(`Total files: ${allFiles.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
|
||||||
const filesExistOnlyInStorage = allFiles.filter((e) => !databaseFileNameCI2CS[e]);
|
|
||||||
const filesExistOnlyInDatabase = allFiles.filter((e) => !storageFileNameCI2CS[e]);
|
|
||||||
const filesExistBoth = allFiles.filter((e) => databaseFileNameCI2CS[e] && storageFileNameCI2CS[e]);
|
|
||||||
|
|
||||||
this._log(`Files exist only in storage: ${filesExistOnlyInStorage.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
|
||||||
this._log(`Files exist only in database: ${filesExistOnlyInDatabase.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
|
||||||
this._log(`Files exist both in storage and database: ${filesExistBoth.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
|
||||||
|
|
||||||
this._log("Synchronising...");
|
|
||||||
const processStatus = {} as Record<string, string>;
|
|
||||||
const logLevel = showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
|
||||||
const updateLog = throttle((key: string, msg: string) => {
|
|
||||||
processStatus[key] = msg;
|
|
||||||
const log = Object.values(processStatus).join("\n");
|
|
||||||
this._log(log, logLevel, "syncAll");
|
|
||||||
}, 25);
|
|
||||||
|
|
||||||
const initProcess = [];
|
|
||||||
const runAll = async <T>(procedureName: string, objects: T[], callback: (arg: T) => Promise<void>) => {
|
|
||||||
if (objects.length == 0) {
|
|
||||||
this._log(`${procedureName}: Nothing to do`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._log(procedureName);
|
|
||||||
if (!this.localDatabase.isReady) throw Error("Database is not ready!");
|
|
||||||
let success = 0;
|
|
||||||
let failed = 0;
|
|
||||||
let total = 0;
|
|
||||||
for await (const result of withConcurrency(
|
|
||||||
objects,
|
|
||||||
async (e) => {
|
|
||||||
try {
|
|
||||||
await callback(e);
|
|
||||||
return true;
|
|
||||||
} catch (ex) {
|
|
||||||
this._log(`Error while ${procedureName}`, LOG_LEVEL_NOTICE);
|
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
10
|
|
||||||
)) {
|
|
||||||
if (result) {
|
|
||||||
success++;
|
|
||||||
} else {
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
total++;
|
|
||||||
const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${objects.length - total}`;
|
|
||||||
updateLog(procedureName, msg);
|
|
||||||
}
|
|
||||||
const msg = `${procedureName} All done: DONE:${success}, FAILED:${failed}`;
|
|
||||||
updateLog(procedureName, msg);
|
|
||||||
};
|
|
||||||
initProcess.push(
|
|
||||||
runAll("UPDATE DATABASE", filesExistOnlyInStorage, async (e) => {
|
|
||||||
// Exists in storage but not in database.
|
|
||||||
const file = storageFileNameMap[storageFileNameCI2CS[e]];
|
|
||||||
if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
|
||||||
const path = file.path;
|
|
||||||
await this.core.fileHandler.storeFileToDB(file);
|
|
||||||
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(path, true));
|
|
||||||
eventHub.emitEvent("event-file-changed", { file: path, automated: true });
|
|
||||||
} else {
|
|
||||||
this._log(`UPDATE DATABASE: ${e} has been skipped due to file size exceeding the limit`, logLevel);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initProcess.push(
|
|
||||||
runAll("UPDATE STORAGE", filesExistOnlyInDatabase, async (e) => {
|
|
||||||
const w = databaseFileNameMap[databaseFileNameCI2CS[e]];
|
|
||||||
// Exists in database but not in storage.
|
|
||||||
const path = this.getPath(w) ?? e;
|
|
||||||
if (w && !(w.deleted || w._deleted)) {
|
|
||||||
if (!this.services.vault.isFileSizeTooLarge(w.size)) {
|
|
||||||
// Prevent applying the conflicted state to the storage.
|
|
||||||
if (w._conflicts?.length ?? 0 > 0) {
|
|
||||||
this._log(`UPDATE STORAGE: ${path} has conflicts. skipped (x)`, LOG_LEVEL_INFO);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// await this.pullFile(path, undefined, false, undefined, false);
|
|
||||||
// Memo: No need to force
|
|
||||||
await this.core.fileHandler.dbToStorage(path, null, true);
|
|
||||||
// fireAndForget(() => this.checkAndApplySettingFromMarkdown(e, true));
|
|
||||||
eventHub.emitEvent("event-file-changed", {
|
|
||||||
file: e,
|
|
||||||
automated: true,
|
|
||||||
});
|
|
||||||
this._log(`Check or pull from db:${path} OK`);
|
|
||||||
} else {
|
|
||||||
this._log(
|
|
||||||
`UPDATE STORAGE: ${path} has been skipped due to file size exceeding the limit`,
|
|
||||||
logLevel
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (w) {
|
|
||||||
this._log(`Deletion history skipped: ${path}`, LOG_LEVEL_VERBOSE);
|
|
||||||
} else {
|
|
||||||
this._log(`entry not found: ${path}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileMap = filesExistBoth.map((path) => {
|
|
||||||
const file = storageFileNameMap[storageFileNameCI2CS[path]];
|
|
||||||
const doc = databaseFileNameMap[databaseFileNameCI2CS[path]];
|
|
||||||
return { file, doc };
|
|
||||||
});
|
|
||||||
initProcess.push(
|
|
||||||
runAll("SYNC DATABASE AND STORAGE", fileMap, async (e) => {
|
|
||||||
const { file, doc } = e;
|
|
||||||
// Prevent applying the conflicted state to the storage.
|
|
||||||
if (doc._conflicts?.length ?? 0 > 0) {
|
|
||||||
this._log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!this.services.vault.isFileSizeTooLarge(file.stat.size) &&
|
|
||||||
!this.services.vault.isFileSizeTooLarge(doc.size)
|
|
||||||
) {
|
|
||||||
await this.syncFileBetweenDBandStorage(file, doc);
|
|
||||||
} else {
|
|
||||||
this._log(
|
|
||||||
`SYNC DATABASE AND STORAGE: ${this.getPath(doc)} has been skipped due to file size exceeding the limit`,
|
|
||||||
logLevel
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(initProcess);
|
|
||||||
|
|
||||||
// this.setStatusBarText(`NOW TRACKING!`);
|
|
||||||
this._log("Initialized, NOW TRACKING!");
|
|
||||||
if (!isInitialized) {
|
|
||||||
await this.core.kvDB.set("initialized", true);
|
|
||||||
}
|
|
||||||
if (showingNotice) {
|
|
||||||
this._log("Initialize done!", LOG_LEVEL_NOTICE, "syncAll");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncFileBetweenDBandStorage(file: UXFileInfoStub, doc: MetaEntry) {
|
|
||||||
if (!doc) {
|
|
||||||
throw new Error(`Missing doc:${(file as any).path}`);
|
|
||||||
}
|
|
||||||
if ("path" in file) {
|
|
||||||
const w = await this.core.storageAccess.getFileStub((file as any).path);
|
|
||||||
if (w) {
|
|
||||||
file = w;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Missing file:${(file as any).path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const compareResult = this.services.path.compareFileFreshness(file, doc);
|
|
||||||
switch (compareResult) {
|
|
||||||
case BASE_IS_NEW:
|
|
||||||
if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
|
||||||
this._log("STORAGE -> DB :" + file.path);
|
|
||||||
await this.core.fileHandler.storeFileToDB(file);
|
|
||||||
} else {
|
|
||||||
this._log(
|
|
||||||
`STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`,
|
|
||||||
LOG_LEVEL_NOTICE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TARGET_IS_NEW:
|
|
||||||
if (!this.services.vault.isFileSizeTooLarge(doc.size)) {
|
|
||||||
this._log("STORAGE <- DB :" + file.path);
|
|
||||||
if (await this.core.fileHandler.dbToStorage(doc, stripAllPrefixes(file.path), true)) {
|
|
||||||
eventHub.emitEvent("event-file-changed", {
|
|
||||||
file: file.path,
|
|
||||||
automated: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._log(`STORAGE <- DB : Cloud not read ${file.path}, possibly deleted`, LOG_LEVEL_NOTICE);
|
|
||||||
}
|
|
||||||
return caches;
|
|
||||||
} else {
|
|
||||||
this._log(
|
|
||||||
`STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`,
|
|
||||||
LOG_LEVEL_NOTICE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case EVEN:
|
|
||||||
this._log("STORAGE == DB :" + file.path + "", LOG_LEVEL_DEBUG);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this._log("STORAGE ?? DB :" + file.path + " Something got weird");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method uses an old version of database accessor, which is not recommended.
|
|
||||||
// TODO: Fix
|
|
||||||
async collectDeletedFiles() {
|
|
||||||
const limitDays = this.settings.automaticallyDeleteMetadataOfDeletedFiles;
|
|
||||||
if (limitDays <= 0) return;
|
|
||||||
this._log(`Checking expired file history`);
|
|
||||||
const limit = Date.now() - 86400 * 1000 * limitDays;
|
|
||||||
const notes: {
|
|
||||||
path: string;
|
|
||||||
mtime: number;
|
|
||||||
ttl: number;
|
|
||||||
doc: PouchDB.Core.ExistingDocument<EntryDoc & PouchDB.Core.AllDocsMeta>;
|
|
||||||
}[] = [];
|
|
||||||
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
|
|
||||||
if (isAnyNote(doc)) {
|
|
||||||
if (doc.deleted && doc.mtime - limit < 0) {
|
|
||||||
notes.push({
|
|
||||||
path: this.getPath(doc),
|
|
||||||
mtime: doc.mtime,
|
|
||||||
ttl: (doc.mtime - limit) / 1000 / 86400,
|
|
||||||
doc: doc,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (notes.length == 0) {
|
|
||||||
this._log("There are no old documents");
|
|
||||||
this._log(`Checking expired file history done`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const v of notes) {
|
|
||||||
this._log(`Deletion history expired: ${v.path}`);
|
|
||||||
const delDoc = v.doc;
|
|
||||||
delDoc._deleted = true;
|
|
||||||
await this.localDatabase.putRaw(delDoc);
|
|
||||||
}
|
|
||||||
this._log(`Checking expired file history done`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _initializeDatabase(
|
|
||||||
showingNotice: boolean = false,
|
|
||||||
reopenDatabase = true,
|
|
||||||
ignoreSuspending: boolean = false
|
|
||||||
): Promise<boolean> {
|
|
||||||
this.services.appLifecycle.resetIsReady();
|
|
||||||
if (
|
|
||||||
!reopenDatabase ||
|
|
||||||
(await this.services.database.openDatabase({
|
|
||||||
databaseEvents: this.services.databaseEvents,
|
|
||||||
replicator: this.services.replicator,
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
if (this.localDatabase.isReady) {
|
|
||||||
await this.services.vault.scanVault(showingNotice, ignoreSuspending);
|
|
||||||
}
|
|
||||||
const ERR_INITIALISATION_FAILED = `Initializing database has been failed on some module!`;
|
|
||||||
if (!(await this.services.databaseEvents.onDatabaseInitialised(showingNotice))) {
|
|
||||||
this.logDetectedError(ERR_INITIALISATION_FAILED, LOG_LEVEL_NOTICE);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.resetDetectedError(ERR_INITIALISATION_FAILED);
|
|
||||||
this.services.appLifecycle.markIsReady();
|
|
||||||
// run queued event once.
|
|
||||||
await this.services.fileProcessing.commitPendingFileEvents();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
this.services.appLifecycle.resetIsReady();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private _reportDetectedErrors(): Promise<string[]> {
|
|
||||||
return Promise.resolve(Array.from(this._detectedErrors));
|
|
||||||
}
|
|
||||||
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
|
||||||
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportDetectedErrors.bind(this));
|
|
||||||
services.databaseEvents.initialiseDatabase.addHandler(this._initializeDatabase.bind(this));
|
|
||||||
services.vault.scanVault.addHandler(this._performFullScan.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
|
||||||
import { sizeToHumanReadable } from "octagonal-wheels/number";
|
|
||||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
|
||||||
import type { LiveSyncCore } from "../../main.ts";
|
|
||||||
import { EVENT_REQUEST_CHECK_REMOTE_SIZE, eventHub } from "@/common/events.ts";
|
|
||||||
import { AbstractModule } from "../AbstractModule.ts";
|
|
||||||
|
|
||||||
export class ModuleCheckRemoteSize extends AbstractModule {
|
|
||||||
checkRemoteSize(): Promise<boolean> {
|
|
||||||
this.settings.notifyThresholdOfRemoteStorageSize = 1;
|
|
||||||
return this._allScanStat();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _allScanStat(): Promise<boolean> {
|
|
||||||
if (this.services.API.isOnline === false) {
|
|
||||||
this._log("Network is offline, skipping remote size check.", LOG_LEVEL_INFO);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
this._log($msg("moduleCheckRemoteSize.logCheckingStorageSizes"), LOG_LEVEL_VERBOSE);
|
|
||||||
if (this.settings.notifyThresholdOfRemoteStorageSize < 0) {
|
|
||||||
const message = $msg("moduleCheckRemoteSize.msgSetDBCapacity");
|
|
||||||
const ANSWER_0 = $msg("moduleCheckRemoteSize.optionNoWarn");
|
|
||||||
const ANSWER_800 = $msg("moduleCheckRemoteSize.option800MB");
|
|
||||||
const ANSWER_2000 = $msg("moduleCheckRemoteSize.option2GB");
|
|
||||||
const ASK_ME_NEXT_TIME = $msg("moduleCheckRemoteSize.optionAskMeLater");
|
|
||||||
|
|
||||||
const ret = await this.core.confirm.askSelectStringDialogue(
|
|
||||||
message,
|
|
||||||
[ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME],
|
|
||||||
{
|
|
||||||
defaultAction: ASK_ME_NEXT_TIME,
|
|
||||||
title: $msg("moduleCheckRemoteSize.titleDatabaseSizeNotify"),
|
|
||||||
timeout: 40,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (ret == ANSWER_0) {
|
|
||||||
this.settings.notifyThresholdOfRemoteStorageSize = 0;
|
|
||||||
await this.saveSettings();
|
|
||||||
} else if (ret == ANSWER_800) {
|
|
||||||
this.settings.notifyThresholdOfRemoteStorageSize = 800;
|
|
||||||
await this.saveSettings();
|
|
||||||
} else if (ret == ANSWER_2000) {
|
|
||||||
this.settings.notifyThresholdOfRemoteStorageSize = 2000;
|
|
||||||
await this.saveSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.settings.notifyThresholdOfRemoteStorageSize > 0) {
|
|
||||||
const remoteStat = await this.core.replicator?.getRemoteStatus(this.settings);
|
|
||||||
if (remoteStat) {
|
|
||||||
const estimatedSize = remoteStat.estimatedSize;
|
|
||||||
if (estimatedSize) {
|
|
||||||
const maxSize = this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024;
|
|
||||||
if (estimatedSize > maxSize) {
|
|
||||||
const message = $msg("moduleCheckRemoteSize.msgDatabaseGrowing", {
|
|
||||||
estimatedSize: sizeToHumanReadable(estimatedSize),
|
|
||||||
maxSize: sizeToHumanReadable(maxSize),
|
|
||||||
});
|
|
||||||
const newMax = ~~(estimatedSize / 1024 / 1024) + 100;
|
|
||||||
const ANSWER_ENLARGE_LIMIT = $msg("moduleCheckRemoteSize.optionIncreaseLimit", {
|
|
||||||
newMax: newMax.toString(),
|
|
||||||
});
|
|
||||||
const ANSWER_REBUILD = $msg("moduleCheckRemoteSize.optionRebuildAll");
|
|
||||||
const ANSWER_IGNORE = $msg("moduleCheckRemoteSize.optionDismiss");
|
|
||||||
const ret = await this.core.confirm.askSelectStringDialogue(
|
|
||||||
message,
|
|
||||||
[ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE],
|
|
||||||
{
|
|
||||||
defaultAction: ANSWER_IGNORE,
|
|
||||||
title: $msg("moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded"),
|
|
||||||
timeout: 60,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (ret == ANSWER_REBUILD) {
|
|
||||||
const ret = await this.core.confirm.askYesNoDialog(
|
|
||||||
$msg("moduleCheckRemoteSize.msgConfirmRebuild"),
|
|
||||||
{ defaultOption: "No" }
|
|
||||||
);
|
|
||||||
if (ret == "yes") {
|
|
||||||
this.core.settings.notifyThresholdOfRemoteStorageSize = -1;
|
|
||||||
await this.saveSettings();
|
|
||||||
await this.core.rebuilder.scheduleRebuild();
|
|
||||||
}
|
|
||||||
} else if (ret == ANSWER_ENLARGE_LIMIT) {
|
|
||||||
this.settings.notifyThresholdOfRemoteStorageSize = ~~(estimatedSize / 1024 / 1024) + 100;
|
|
||||||
this._log(
|
|
||||||
$msg("moduleCheckRemoteSize.logThresholdEnlarged", {
|
|
||||||
size: this.settings.notifyThresholdOfRemoteStorageSize.toString(),
|
|
||||||
}),
|
|
||||||
LOG_LEVEL_NOTICE
|
|
||||||
);
|
|
||||||
// await this.core.saveSettings();
|
|
||||||
await this.core.services.setting.saveSettingData();
|
|
||||||
} else {
|
|
||||||
// Dismiss or Close the dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
this._log(
|
|
||||||
$msg("moduleCheckRemoteSize.logExceededWarning", {
|
|
||||||
measuredSize: sizeToHumanReadable(estimatedSize),
|
|
||||||
notifySize: sizeToHumanReadable(
|
|
||||||
this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
LOG_LEVEL_INFO
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this._log(
|
|
||||||
$msg("moduleCheckRemoteSize.logCurrentStorageSize", {
|
|
||||||
measuredSize: sizeToHumanReadable(estimatedSize),
|
|
||||||
}),
|
|
||||||
LOG_LEVEL_INFO
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _everyOnloadStart(): Promise<boolean> {
|
|
||||||
this.addCommand({
|
|
||||||
id: "livesync-reset-remote-size-threshold-and-check",
|
|
||||||
name: "Reset notification threshold and check the remote database usage",
|
|
||||||
callback: async () => {
|
|
||||||
await this.checkRemoteSize();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
eventHub.onEvent(EVENT_REQUEST_CHECK_REMOTE_SIZE, () => this.checkRemoteSize());
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
|
||||||
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
|
||||||
services.appLifecycle.onScanningStartupIssues.addHandler(this._allScanStat.bind(this));
|
|
||||||
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user