Compare commits

...

6 Commits

Author SHA1 Message Date
vorotamoroz
ad18140a37 Improved an error verbosity on concurrent processing on start-up process. 2026-05-18 12:04:09 +01:00
vorotamoroz
d6bf453a6d Merge branch 'main' of https://github.com/vrtmrz/obsidian-livesync 2026-05-18 11:07:43 +01:00
vorotamoroz
e80cdc2dae remove unused files 2026-05-18 08:47:03 +01:00
vorotamoroz
60780678fd Merge pull request #903 from Joysimple/cli-docker
bugfix: Add package-lock.json into docker build
2026-05-18 12:38:12 +09:00
vorotamoroz
273e7a2b63 Merge pull request #909 from vrtmrz/p2p_config_selectable
P2p config selectable (a.k.a. v0.25.64)
2026-05-17 13:24:40 +09:00
Nikolay Sokolov
02673a1631 bugfix: Add package-lock.json into docker build 2026-05-14 23:17:37 -07:00
7 changed files with 9 additions and 922 deletions

View File

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

Submodule src/lib updated: a0af792b48...36b99354f6

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -3,6 +3,13 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## Unreleased (0.25.64-patch1)
18th May, 2026
### Improved
- Improved an error verbosity on concurrent processing on start-up process.
## 0.25.64 ## 0.25.64
17th May, 2026 17th May, 2026