diff --git a/src/lib b/src/lib index 9888ee8..b8e4fa6 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 9888ee8859cffd08d8ea0996be9da8f31d026511 +Subproject commit b8e4fa6b9e73f1cd5df739c05be1496c48c7a71c diff --git a/src/modules/core/ModuleFileHandler.ts b/src/modules/core/ModuleFileHandler.ts index 69a0d90..3a32d91 100644 --- a/src/modules/core/ModuleFileHandler.ts +++ b/src/modules/core/ModuleFileHandler.ts @@ -297,7 +297,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { } await this.storage.ensureDir(path); const ret = await this.storage.writeFileAuto(path, docData, { ctime: docRead.ctime, mtime: docRead.mtime }); - this.storage.touched(path); + await this.storage.touched(path); this.storage.triggerFileEvent(mode, path); return ret; } diff --git a/src/modules/core/ModuleReplicator.ts b/src/modules/core/ModuleReplicator.ts index 275fb4f..2bce704 100644 --- a/src/modules/core/ModuleReplicator.ts +++ b/src/modules/core/ModuleReplicator.ts @@ -30,6 +30,7 @@ import { isAnyNote } from "../../lib/src/common/utils"; import { EVENT_FILE_SAVED, eventHub } from "../../common/events"; import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator"; import { globalSlipBoard } from "../../lib/src/bureau/bureau"; +import { $msg } from "../../lib/src/common/i18n"; const KEY_REPLICATION_ON_EVENT = "replicationOnEvent"; const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000; @@ -46,7 +47,7 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule { async setReplicator() { const replicator = await this.core.$anyNewReplicator(); if (!replicator) { - this._log("No replicator is available, this is the fatal error.", LOG_LEVEL_NOTICE); + this._log($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE); return false; } this.core.replicator = replicator; @@ -79,23 +80,82 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule { updatePreviousExecutionTime(KEY_REPLICATION_ON_EVENT); } } + + /** + * obsolete method. No longer maintained and will be removed in the future. + * @deprecated v0.24.17 + * @param showMessage If true, show message to the user. + */ + async cleaned(showMessage: boolean) { + 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. +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. +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_CLEAN = "Cleanup"; + const CHOICE_DISMISS = "Dismiss"; + const ret = await this.core.confirm.confirmWithMessage( + "Cleaned", + message, + [CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS], + CHOICE_DISMISS, + 30 + ); + if (ret == CHOICE_FETCH) { + await this.core.rebuilder.$performRebuildDB("localOnly"); + } + if (ret == CHOICE_CLEAN) { + const replicator = this.core.$$getReplicator(); + if (!(replicator instanceof LiveSyncCouchDBReplicator)) return; + const remoteDB = await replicator.connectRemoteCouchDBWithSetting( + this.settings, + this.core.$$isMobile(), + 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.core.replicator.openReplication(this.settings, false, showMessage, true)) { + await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db); + await purgeUnreferencedChunks(this.localDatabase.localDatabase, false); + this.localDatabase.hashCaches.clear(); + await this.core.$$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 + ); + } + } + }); + } async $$_replicate(showMessage: boolean = false): Promise { //--? if (!this.core.$$isReady()) return; if (isLockAcquired("cleanup")) { - Logger("Database cleaning up is in process. replication has been cancelled", LOG_LEVEL_NOTICE); + Logger($msg("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE); return; } if (this.settings.versionUpFlash != "") { - Logger("Open settings and check message, please. replication has been cancelled.", LOG_LEVEL_NOTICE); + Logger($msg("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE); return; } if (!(await this.core.$everyCommitPendingFileEvent())) { - Logger("Some file events are pending. Replication has been cancelled.", LOG_LEVEL_NOTICE); + Logger($msg("Replicator.Message.Pending"), LOG_LEVEL_NOTICE); return false; } if (!(await this.core.$everyBeforeReplicate(showMessage))) { - Logger(`Replication has been cancelled by some module failure`, LOG_LEVEL_NOTICE); + Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE); return false; } @@ -107,106 +167,35 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule { } else { if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) { if (this.core.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. -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. -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_CLEAN = "Cleanup"; - const CHOICE_DISMISS = "Dismiss"; - const ret = await this.core.confirm.confirmWithMessage( - "Cleaned", - message, - [CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS], - CHOICE_DISMISS, - 30 - ); - if (ret == CHOICE_FETCH) { - await this.core.rebuilder.$performRebuildDB("localOnly"); - } - if (ret == CHOICE_CLEAN) { - const replicator = this.core.$$getReplicator(); - if (!(replicator instanceof LiveSyncCouchDBReplicator)) return; - const remoteDB = await replicator.connectRemoteCouchDBWithSetting( - this.settings, - this.core.$$isMobile(), - 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.core.replicator.openReplication(this.settings, false, showMessage, true) - ) { - await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db); - await purgeUnreferencedChunks(this.localDatabase.localDatabase, false); - this.localDatabase.hashCaches.clear(); - await this.core.$$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 - ); - } - } - }); + await this.cleaned(showMessage); } else { - const message = ` -The remote database has been rebuilt. -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. - `; - const CHOICE_FETCH = "Fetch again"; - const CHOICE_DISMISS = "Dismiss"; - const ret = await this.core.confirm.confirmWithMessage( - "Locked", + const message = $msg("Replicator.Dialogue.Locked.Message"); + const CHOICE_FETCH = $msg("Replicator.Dialogue.Locked.Action.Fetch"); + const CHOICE_DISMISS = $msg("Replicator.Dialogue.Locked.Action.Dismiss"); + const CHOICE_UNLOCK = $msg("Replicator.Dialogue.Locked.Action.Unlock"); + const ret = await this.core.confirm.askSelectStringDialogue( message, - [CHOICE_FETCH, CHOICE_DISMISS], - CHOICE_DISMISS, - 10 + [CHOICE_FETCH, CHOICE_UNLOCK, CHOICE_DISMISS], + { + title: $msg("Replicator.Dialogue.Locked.Title"), + defaultAction: CHOICE_DISMISS, + timeout: 60, + } ); if (ret == CHOICE_FETCH) { - const CHOICE_RESTART = "Restart"; - const CHOICE_WITHOUT_RESTART = "Without restart"; - if ( - (await this.core.confirm.askSelectStringDialogue( - "Self-hosted LiveSync restarts Obsidian to fetch everything safely. However, you can do it without restarting. Please choose one.", - [CHOICE_RESTART, CHOICE_WITHOUT_RESTART], - { - title: "Fetch again", - defaultAction: CHOICE_RESTART, - timeout: 30, - } - )) == CHOICE_RESTART - ) { - await this.core.rebuilder.scheduleFetch(); - // await this.core.$$scheduleAppReload(); - return; - } else { - await this.core.rebuilder.$performRebuildDB("localOnly"); - } + this._log($msg("Replicator.Dialogue.Locked.Message.Fetch"), LOG_LEVEL_NOTICE); + await this.core.rebuilder.scheduleFetch(); + this.core.$$scheduleAppReload(); + return; + } else if (ret == CHOICE_UNLOCK) { + await this.core.replicator.markRemoteResolved(this.settings); + this._log($msg("Replicator.Dialogue.Locked.Message.Unlocked"), LOG_LEVEL_NOTICE); + return; } } } } } - return ret; } @@ -414,7 +403,7 @@ Or if you are sure know what had been happened, we can unlock the database from ): Promise { if (!this.core.$$isReady()) return false; if (!(await this.core.$everyBeforeReplicate(showingNotice))) { - Logger(`Replication has been cancelled by some module failure`, LOG_LEVEL_NOTICE); + Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE); return false; } if (!sendChunksInBulkDisabled) { diff --git a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts index ca00ea1..2e703a7 100644 --- a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts +++ b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts @@ -2,14 +2,17 @@ import { Logger, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; import { extractObject } from "octagonal-wheels/object"; import { TweakValuesShouldMatchedTemplate, - CompatibilityBreakingTweakValues, + IncompatibleChanges, confName, type TweakValues, type RemoteDBSettings, + IncompatibleChangesInSpecificPattern, + CompatibleButLossyChanges, } from "../../lib/src/common/types.ts"; import { escapeMarkdownValue } from "../../lib/src/common/utils.ts"; import { AbstractModule } from "../AbstractModule.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; +import { $msg } from "../../lib/src/common/i18n.ts"; export class ModuleResolvingMismatchedTweaks extends AbstractModule implements ICoreModule { async $anyAfterConnectCheckFailed(): Promise { @@ -28,65 +31,100 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings); const items = Object.entries(TweakValuesShouldMatchedTemplate); let rebuildRequired = false; - + let rebuildRecommended = false; // Making tables: - let table = `| Value name | This device | Configured | \n` + `|: --- |: --- :|: ---- :| \n`; - + // let table = `| Value name | This device | Configured | \n` + `|: --- |: --- :|: ---- :| \n`; + const tableRows = []; // const items = [mine,preferred] for (const v of items) { const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate; const valueMine = escapeMarkdownValue(mine[key]); const valuePreferred = escapeMarkdownValue(preferred[key]); if (valueMine == valuePreferred) continue; - if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) { + if (IncompatibleChanges.indexOf(key) !== -1) { rebuildRequired = true; } - table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`; + for (const pattern of IncompatibleChangesInSpecificPattern) { + if (pattern.key !== key) continue; + // if from value supplied, check if current value have been violated : in other words, if the current value is the same as the from value, it should require a rebuild. + const isFromConditionMet = "from" in pattern ? pattern.from === mine[key] : false; + // and, if to value supplied, same as above. + const isToConditionMet = "to" in pattern ? pattern.to === preferred[key] : false; + // if either of them is true, it should require a rebuild, if the pattern is not a recommendation. + if (isFromConditionMet || isToConditionMet) { + if (pattern.isRecommendation) { + rebuildRecommended = true; + } else { + rebuildRequired = true; + } + } + } + if (CompatibleButLossyChanges.indexOf(key) !== -1) { + rebuildRecommended = true; + } + + // table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`; + tableRows.push( + $msg("TweakMismatchResolve.Table.Row", { + name: confName(key), + self: valueMine, + remote: valuePreferred, + }) + ); } - const additionalMessage = rebuildRequired - ? ` + const additionalMessage = + rebuildRequired && this.core.settings.isConfigured + ? $msg("TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired") + : ""; + const additionalMessage2 = + rebuildRecommended && this.core.settings.isConfigured + ? $msg("TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended") + : ""; -**Note**: We have detected that some of the values are different to make incompatible the local database with the remote database. -If you choose to use the configured values, the local database will be rebuilt, and if you choose to use the values of this device, the remote database will be rebuilt. -Both of them takes a few minutes. Please choose after considering the situation.` - : ""; + const table = $msg("TweakMismatchResolve.Table", { rows: tableRows.join("\n") }); - const message = ` -Your configuration has not been matched with the one on the remote server. -(Which you had decided once before, or set by initially synchronised device). + const message = $msg("TweakMismatchResolve.Message.MainTweakResolving", { + table: table, + additionalMessage: [additionalMessage, additionalMessage2].filter((v) => v).join("\n"), + }); -Configured values: + const CHOICE_USE_REMOTE = $msg("TweakMismatchResolve.Action.UseRemote"); + const CHOICE_USE_REMOTE_WITH_REBUILD = $msg("TweakMismatchResolve.Action.UseRemoteWithRebuild"); + const CHOICE_USE_REMOTE_PREVENT_REBUILD = $msg("TweakMismatchResolve.Action.UseRemoteAcceptIncompatible"); + const CHOICE_USE_MINE = $msg("TweakMismatchResolve.Action.UseMine"); + const CHOICE_USE_MINE_WITH_REBUILD = $msg("TweakMismatchResolve.Action.UseMineWithRebuild"); + const CHOICE_USE_MINE_PREVENT_REBUILD = $msg("TweakMismatchResolve.Action.UseMineAcceptIncompatible"); + const CHOICE_DISMISS = $msg("TweakMismatchResolve.Action.Dismiss"); -${table} + const CHOICE_AND_VALUES = [] as [string, [result: TweakValues | boolean, rebuild: boolean]][]; -Please select which one you want to use. - -- Use configured: Update settings of this device by configured one on the remote server. - You should select this if you have changed the settings on ** another device **. -- Update with mine: Update settings on the remote server by the settings of this device. - You should select this if you have changed the settings on ** this device **. -- Dismiss: Ignore this message and keep the current settings. - You cannot synchronise until you resolve this issue without enabling \`Do not check configuration mismatch before replication\`.${additionalMessage}`; - - const CHOICE_USE_REMOTE = "Use configured"; - const CHOICE_USR_MINE = "Update with mine"; - const CHOICE_DISMISS = "Dismiss"; - const CHOICE_AND_VALUES = [ - [CHOICE_USE_REMOTE, preferred], - [CHOICE_USR_MINE, true], - [CHOICE_DISMISS, false], - ]; - const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record; - const retKey = await this.core.confirm.confirmWithMessage( - "Tweaks Mismatched or Changed", - message, - Object.keys(CHOICES), - CHOICE_DISMISS, - 60 - ); + if (rebuildRequired) { + CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [preferred, true]]); + CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]); + CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_PREVENT_REBUILD, [preferred, false]]); + CHOICE_AND_VALUES.push([CHOICE_USE_MINE_PREVENT_REBUILD, [true, false]]); + } else if (rebuildRecommended) { + CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]); + CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]); + CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [true, true]]); + CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]); + } else { + CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]); + CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]); + } + CHOICE_AND_VALUES.push([CHOICE_DISMISS, [false, false]]); + const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record< + string, + [TweakValues | boolean, performRebuild: boolean] + >; + const retKey = await this.core.confirm.askSelectStringDialogue(message, Object.keys(CHOICES), { + title: $msg("TweakMismatchResolve.Title.TweakResolving"), + timeout: 60, + defaultAction: CHOICE_DISMISS, + }); if (!retKey) return [false, false]; - return [CHOICES[retKey], rebuildRequired]; + return CHOICES[retKey]; } async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> { @@ -143,28 +181,56 @@ Please select which one you want to use. return { result: false, requireFetch: false }; } } + async $$askUseRemoteConfiguration( trialSetting: RemoteDBSettings, preferred: TweakValues ): Promise<{ result: false | TweakValues; requireFetch: boolean }> { const items = Object.entries(TweakValuesShouldMatchedTemplate); let rebuildRequired = false; + let rebuildRecommended = false; // Making tables: - let table = `| Value name | This device | Stored | \n` + `|: --- |: ---- :|: ---- :| \n`; + // let table = `| Value name | This device | On Remote | \n` + `|: --- |: ---- :|: ---- :| \n`; let differenceCount = 0; + const tableRows = [] as string[]; // const items = [mine,preferred] for (const v of items) { const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate; - const valuePreferred = escapeMarkdownValue(preferred[key]); - const currentDisp = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])} |`; + const remoteValueForDisplay = escapeMarkdownValue(preferred[key]); + const currentValueForDisplay = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])}`; if ((trialSetting as TweakValues)?.[key] !== preferred[key]) { - if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) { + if (IncompatibleChanges.indexOf(key) !== -1) { rebuildRequired = true; } + for (const pattern of IncompatibleChangesInSpecificPattern) { + if (pattern.key !== key) continue; + // if from value supplied, check if current value have been violated : in other words, if the current value is the same as the from value, it should require a rebuild. + const isFromConditionMet = + "from" in pattern ? pattern.from === (trialSetting as TweakValues)?.[key] : false; + // and, if to value supplied, same as above. + const isToConditionMet = "to" in pattern ? pattern.to === preferred[key] : false; + // if either of them is true, it should require a rebuild, if the pattern is not a recommendation. + if (isFromConditionMet || isToConditionMet) { + if (pattern.isRecommendation) { + rebuildRecommended = true; + } else { + rebuildRequired = true; + } + } + } + if (CompatibleButLossyChanges.indexOf(key) !== -1) { + rebuildRecommended = true; + } } else { continue; } - table += `| ${confName(key)} | ${currentDisp} ${valuePreferred} | \n`; + tableRows.push( + $msg("TweakMismatchResolve.Table.Row", { + name: confName(key), + self: currentValueForDisplay, + remote: remoteValueForDisplay, + }) + ); differenceCount++; } @@ -174,33 +240,28 @@ Please select which one you want to use. } const additionalMessage = rebuildRequired && this.core.settings.isConfigured - ? ` - ->[!WARNING] -> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required. -***Please ensure that you have time and are connected to a stable network to apply!***` + ? $msg("TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired") + : ""; + const additionalMessage2 = + rebuildRecommended && this.core.settings.isConfigured + ? $msg("TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended") : ""; - const message = ` -The settings in the remote database are as follows. -If you want to use these settings, please select "Use configured". -If you want to keep the settings of this device, please select "Dismiss". + const table = $msg("TweakMismatchResolve.Table", { rows: tableRows.join("\n") }); -${table} + const message = $msg("TweakMismatchResolve.Message.Main", { + table: table, + additionalMessage: [additionalMessage, additionalMessage2].filter((v) => v).join("\n"), + }); ->[!TIP] -> If you want to synchronise all settings, please use \`Sync settings via markdown\` after applying minimal configuration with this feature. - -${additionalMessage}`; - - const CHOICE_USE_REMOTE = "Use configured"; - const CHOICE_DISMISS = "Dismiss"; + const CHOICE_USE_REMOTE = $msg("TweakMismatchResolve.Action.UseConfigured"); + const CHOICE_DISMISS = $msg("TweakMismatchResolve.Action.Dismiss"); // const CHOICE_AND_VALUES = [ // [CHOICE_USE_REMOTE, preferred], // [CHOICE_DISMISS, false]] const CHOICES = [CHOICE_USE_REMOTE, CHOICE_DISMISS]; const retKey = await this.core.confirm.askSelectStringDialogue(message, CHOICES, { - title: "Use Remote Configuration", + title: $msg("TweakMismatchResolve.Title.UseRemoteConfig"), timeout: 0, defaultAction: CHOICE_DISMISS, }); diff --git a/src/modules/coreObsidian/ModuleFileAccessObsidian.ts b/src/modules/coreObsidian/ModuleFileAccessObsidian.ts index 8d61a5d..2351e39 100644 --- a/src/modules/coreObsidian/ModuleFileAccessObsidian.ts +++ b/src/modules/coreObsidian/ModuleFileAccessObsidian.ts @@ -1,4 +1,4 @@ -import { normalizePath, TFile, TFolder, type ListedFiles } from "obsidian"; +import { TFile, TFolder, type ListedFiles } from "obsidian"; import { SerializedFileAccess } from "./storageLib/SerializedFileAccess"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; @@ -60,7 +60,23 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements if (file instanceof TFile) { return this.vaultAccess.vaultModify(file, data, opt); } else if (file === null) { - return (await this.vaultAccess.vaultCreate(path, data, opt)) instanceof TFile; + if (!path.endsWith(".md")) { + // Very rare case, we encountered this case with `writing-goals-history.csv` file. + // Indeed, that file not appears in the File Explorer, but it exists in the vault. + // Hence, we cannot retrieve the file from the vault by getAbstractFileByPath, and we cannot write it via vaultModify. + // It makes `File already exists` error. + // Therefore, we need to write it via adapterWrite. + // Maybe there are others like this, so I will write it via adapterWrite. + // This is a workaround for the issue, but I don't know if this is the right solution. + // (So limits to non-md files). + // Has Obsidian been patched?, anyway, writing directly might be a safer approach. + // However, does changes of that file trigger file-change event? + await this.vaultAccess.adapterWrite(path, data, opt); + // For safety, check existence + return await this.vaultAccess.adapterExists(path); + } else { + return (await this.vaultAccess.vaultCreate(path, data, opt)) instanceof TFile; + } } else { this._log(`Could not write file (Possibly already exists as a folder): ${path}`, LOG_LEVEL_VERBOSE); return false; @@ -158,8 +174,9 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements } } triggerFileEvent(event: string, path: string): void { - // this.app.vault.trigger("file-change", path); - this.vaultAccess.trigger(event, this.vaultAccess.getAbstractFileByPath(normalizePath(path))); + const file = this.vaultAccess.getAbstractFileByPath(path); + if (file === null) return; + this.vaultAccess.trigger(event, file); } async triggerHiddenFile(path: string): Promise { //@ts-ignore internal function @@ -258,9 +275,9 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements } return files as FilePath[]; } - touched(file: UXFileInfoStub | FilePathWithPrefix): void { + async touched(file: UXFileInfoStub | FilePathWithPrefix): Promise { const path = typeof file === "string" ? file : file.path; - this.vaultAccess.touch(path as FilePath); + await this.vaultAccess.touch(path as FilePath); } recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean { const xFile = typeof file === "string" ? (this.vaultAccess.getAbstractFileByPath(file) as TFile) : file; diff --git a/src/modules/coreObsidian/storageLib/SerializedFileAccess.ts b/src/modules/coreObsidian/storageLib/SerializedFileAccess.ts index 42a8388..1a27533 100644 --- a/src/modules/coreObsidian/storageLib/SerializedFileAccess.ts +++ b/src/modules/coreObsidian/storageLib/SerializedFileAccess.ts @@ -199,9 +199,15 @@ export class SerializedFileAccess { touchedFiles: string[] = []; - touch(file: TFile | FilePath) { - const f = file instanceof TFile ? file : (this.getAbstractFileByPath(file) as TFile); - const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`; + _statInternal(file: FilePath) { + return this.app.vault.adapter.stat(file); + } + + async touch(file: TFile | FilePath) { + const path = file instanceof TFile ? (file.path as FilePath) : file; + const statOrg = file instanceof TFile ? file.stat : await this._statInternal(path); + const stat = statOrg || { mtime: 0, size: 0 }; + const key = `${path}-${stat.mtime}-${stat.size}`; this.touchedFiles.unshift(key); this.touchedFiles = this.touchedFiles.slice(0, 100); } diff --git a/src/modules/essential/ModuleMigration.ts b/src/modules/essential/ModuleMigration.ts index d7219c6..2965870 100644 --- a/src/modules/essential/ModuleMigration.ts +++ b/src/modules/essential/ModuleMigration.ts @@ -1,4 +1,4 @@ -import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js"; +import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js"; import { type ObsidianLiveSyncSettings } from "../../lib/src/common/types.js"; import { EVENT_REQUEST_OPEN_P2P, @@ -25,7 +25,10 @@ export class ModuleMigration extends AbstractModule implements ICoreModule { } const issues = Object.entries(r.rules); if (issues.length == 0) { - this._log($msg("Doctor.Message.NoIssues"), LOG_LEVEL_NOTICE); + this._log( + $msg("Doctor.Message.NoIssues"), + activateReason !== "updated" ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO + ); return; } else { const OPT_YES = `${$msg("Doctor.Button.Yes")}` as const; @@ -68,7 +71,7 @@ export class ModuleMigration extends AbstractModule implements ICoreModule { [RuleLevel.Must]: $msg("Doctor.Level.Must"), }; const level = value.level ? levelMap[value.level] : "Unknown"; - const options = [OPT_FIX]; + const options = [OPT_FIX] as [typeof OPT_FIX | typeof OPT_SKIP | typeof OPT_FIXBUTNOREBUILD]; if ((!skipRebuild && value.requireRebuild) || value.requireRebuildLocal) { options.push(OPT_FIXBUTNOREBUILD); } diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index d65f0b0..d218ecc 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -1614,8 +1614,33 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal const newTweaks = await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); if (newTweaks.result !== false) { - this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; - this.requestUpdate(); + if (this.inWizard) { + this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; + this.requestUpdate(); + return; + } else { + this.closeSetting(); + this.plugin.settings = { ...this.plugin.settings, ...newTweaks.result }; + if (newTweaks.requireFetch) { + if ( + (await this.plugin.confirm.askYesNoDialog( + $msg("SettingTab.Message.AskRebuild"), + { + defaultOption: "Yes", + } + )) == "no" + ) { + await this.plugin.$$saveSettingData(); + return; + } + await this.plugin.$$saveSettingData(); + await this.plugin.rebuilder.scheduleFetch(); + await this.plugin.$$scheduleAppReload(); + return; + } else { + await this.plugin.$$saveSettingData(); + } + } } }) ); diff --git a/src/modules/interfaces/StorageAccess.ts b/src/modules/interfaces/StorageAccess.ts index 010a1d6..533fe4e 100644 --- a/src/modules/interfaces/StorageAccess.ts +++ b/src/modules/interfaces/StorageAccess.ts @@ -38,7 +38,7 @@ export interface StorageAccess { getFiles(): UXFileInfoStub[]; getFileNames(): FilePathWithPrefix[]; - touched(file: UXFileInfoStub | FilePathWithPrefix): void; + touched(file: UXFileInfoStub | FilePathWithPrefix): Promise; recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean; clearTouched(): void;