mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-01 08:17:07 +00:00
Distinguish absent and unreadable remote settings during setup
Use typed Commonlib outcomes to guide Fetch and Rebuild without treating a new remote as a failed read. Preserve automatic synchronisation choices when scheduled initialisation is cancelled.
This commit is contained in:
+129
-62
@@ -10,7 +10,10 @@ import RebuildEverything from "@/modules/features/SetupWizard/dialogs/RebuildEve
|
||||
import { extractObject } from "octagonal-wheels/object";
|
||||
import { REMOTE_MINIO, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { TweakValuesShouldMatchedTemplate } from "@vrtmrz/livesync-commonlib/compat/common/models/tweak.definition";
|
||||
import {
|
||||
RemotePreferredTweakStatuses,
|
||||
TweakValuesShouldMatchedTemplate,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/models/tweak.definition";
|
||||
import type {
|
||||
FetchEverythingResult,
|
||||
RebuildEverythingResult,
|
||||
@@ -19,6 +22,7 @@ import { askAndPerformFastSetupOnScheduledFetchAll } from "./redFlag.simpleFetch
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations";
|
||||
import { isP2PMainRemote } from "@/common/remoteConfiguration";
|
||||
import { $msg } from "@/common/translation";
|
||||
|
||||
/**
|
||||
* Flag file handler interface, similar to target filter pattern.
|
||||
@@ -29,6 +33,8 @@ interface FlagFileHandler {
|
||||
handle: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
type InitialisationOperation = "fetch" | "rebuild";
|
||||
|
||||
export async function isFlagFileExist(host: NecessaryServices<never, "storageAccess">, path: string) {
|
||||
const redFlagExist = await host.serviceModules.storageAccess.isExists(
|
||||
host.serviceModules.storageAccess.normalisePath(path)
|
||||
@@ -158,9 +164,7 @@ export function createFetchAllFlagHandler(
|
||||
await host.services.UI.dialogManager.openWithExplicitCancel<FetchEverythingResult>(FetchEverything);
|
||||
if (method === "cancelled") {
|
||||
log("Fetch everything cancelled by user.", LOG_LEVEL_NOTICE);
|
||||
await cleanupFlag();
|
||||
host.services.appLifecycle.performRestart();
|
||||
return false;
|
||||
return await cancelScheduledInitialisation(host, cleanupFlag);
|
||||
}
|
||||
const { vault, extra } = method;
|
||||
const settings = await Promise.resolve(host.services.setting.currentSettings());
|
||||
@@ -185,9 +189,11 @@ export function createFetchAllFlagHandler(
|
||||
},
|
||||
} as const;
|
||||
|
||||
if (!(await adjustSettingToRemoteIfNeeded(host, log, extra, settings))) {
|
||||
log("Fetch initialisation cancelled by user.", LOG_LEVEL_NOTICE);
|
||||
return await cancelScheduledInitialisation(host, cleanupFlag);
|
||||
}
|
||||
return await processVaultInitialisation(host, log, async () => {
|
||||
const settings = host.services.setting.currentSettings();
|
||||
await adjustSettingToRemoteIfNeeded(host, log, extra, settings);
|
||||
const vaultStateToAction = mapVaultStateToAction[vault];
|
||||
const { makeLocalChunkBeforeSync, makeLocalFilesBeforeSync } = vaultStateToAction;
|
||||
log(
|
||||
@@ -217,93 +223,153 @@ export function createFetchAllFlagHandler(
|
||||
/**
|
||||
* Adjust setting to remote configuration.
|
||||
* @param config current configuration to retrieve remote preferred config
|
||||
* @returns updated configuration if applied, otherwise null.
|
||||
* @param operation operation which will consume the selected configuration
|
||||
* @returns whether initialisation may continue.
|
||||
*/
|
||||
export async function adjustSettingToRemote(
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", never>,
|
||||
log: LogFunction,
|
||||
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 host.services.tweakValue.fetchRemotePreferred(config);
|
||||
if (!remoteTweaks) {
|
||||
config: ObsidianLiveSyncSettings,
|
||||
operation: InitialisationOperation = "fetch"
|
||||
): Promise<boolean> {
|
||||
while (true) {
|
||||
const remoteResult = await host.services.tweakValue.fetchRemotePreferred(config);
|
||||
if (remoteResult.status === RemotePreferredTweakStatuses.NOT_CONFIGURED) {
|
||||
const useDeviceSettings = $msg("Use this device's settings");
|
||||
const cancelInitialisation = $msg("Cancel");
|
||||
log(`Remote synchronisation settings are not configured (${remoteResult.reason}).`, LOG_LEVEL_INFO);
|
||||
const choice = await host.services.UI.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,
|
||||
$msg(
|
||||
"The selected remote has no saved synchronisation settings. This is normal for a new remote. Use this device's settings, or cancel if you expected existing settings."
|
||||
),
|
||||
[useDeviceSettings, cancelInitialisation] as const,
|
||||
{
|
||||
defaultAction: RETRY_FETCH,
|
||||
defaultAction: useDeviceSettings,
|
||||
timeout: 0,
|
||||
title: "Fetch Remote Configuration Failed",
|
||||
title: $msg("No Synchronisation Settings Found"),
|
||||
}
|
||||
);
|
||||
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[key as keyof ObsidianLiveSyncSettings] !== value;
|
||||
});
|
||||
if (differentItems.length === 0) {
|
||||
log("Remote configuration matches local configuration. No changes applied.", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
await host.services.UI.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,
|
||||
return choice === useDeviceSettings;
|
||||
}
|
||||
if (remoteResult.status === RemotePreferredTweakStatuses.UNAVAILABLE) {
|
||||
const retryRemoteSettings = $msg("Retry");
|
||||
const useDeviceSettings = $msg("Use this device's settings");
|
||||
const cancelInitialisation = $msg("Cancel");
|
||||
log("Could not read synchronisation settings from the remote.", LOG_LEVEL_NOTICE);
|
||||
log(remoteResult.error, LOG_LEVEL_VERBOSE);
|
||||
if (operation === "rebuild") {
|
||||
const choice = await host.services.UI.confirm.askSelectStringDialogue(
|
||||
$msg(
|
||||
"Could not read the remote's synchronisation settings. Retry, or continue the overwrite with this device's settings. A working connection is still required."
|
||||
),
|
||||
[retryRemoteSettings, useDeviceSettings, cancelInitialisation] as const,
|
||||
{
|
||||
defaultAction: "OK",
|
||||
defaultAction: retryRemoteSettings,
|
||||
timeout: 0,
|
||||
title: $msg("Could Not Read Synchronisation Settings"),
|
||||
}
|
||||
);
|
||||
if (choice === retryRemoteSettings) continue;
|
||||
return choice === useDeviceSettings;
|
||||
}
|
||||
|
||||
config = {
|
||||
...config,
|
||||
...(Object.fromEntries(differentItems) as Partial<ObsidianLiveSyncSettings>),
|
||||
} satisfies ObsidianLiveSyncSettings;
|
||||
await host.services.setting.applyExternalSettings(config, true);
|
||||
log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
||||
canProceed = true;
|
||||
const updatedConfig = host.services.setting.currentSettings();
|
||||
return updatedConfig;
|
||||
const choice = await host.services.UI.confirm.askSelectStringDialogue(
|
||||
$msg(
|
||||
"Could not read the remote's synchronisation settings. Check the connection and credentials, then retry."
|
||||
),
|
||||
[retryRemoteSettings, cancelInitialisation] as const,
|
||||
{
|
||||
defaultAction: retryRemoteSettings,
|
||||
timeout: 0,
|
||||
title: $msg("Could Not Read Synchronisation Settings"),
|
||||
}
|
||||
);
|
||||
if (choice === retryRemoteSettings) continue;
|
||||
return false;
|
||||
}
|
||||
} while (!canProceed);
|
||||
if (remoteResult.status === RemotePreferredTweakStatuses.UNSUPPORTED) {
|
||||
log("Remote synchronisation settings are not supported by this remote type.", LOG_LEVEL_INFO);
|
||||
return true;
|
||||
}
|
||||
|
||||
const remoteTweaks = remoteResult.values;
|
||||
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[key as keyof ObsidianLiveSyncSettings] !== value;
|
||||
});
|
||||
if (differentItems.length === 0) {
|
||||
log("Remote configuration matches local configuration. No changes applied.", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
await host.services.UI.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) as Partial<ObsidianLiveSyncSettings>),
|
||||
} satisfies ObsidianLiveSyncSettings;
|
||||
await host.services.setting.applyExternalSettings(config, true);
|
||||
log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param operation operation which will consume the selected configuration
|
||||
*/
|
||||
export async function adjustSettingToRemoteIfNeeded(
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", never>,
|
||||
log: LogFunction,
|
||||
extra: { preventFetchingConfig: boolean },
|
||||
config: ObsidianLiveSyncSettings
|
||||
) {
|
||||
if (extra && extra.preventFetchingConfig) {
|
||||
return;
|
||||
extra: { preventFetchingConfig: boolean } | null,
|
||||
config: ObsidianLiveSyncSettings,
|
||||
operation: InitialisationOperation = "fetch"
|
||||
): Promise<boolean> {
|
||||
if (extra?.preventFetchingConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// P2P has no centralised remote configuration; skip to avoid a spurious
|
||||
// "Failed to connect to the remote server" error dialog.
|
||||
if (config.remoteType === REMOTE_P2P) {
|
||||
log("Remote configuration fetch skipped (P2P mode).", LOG_LEVEL_INFO);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remote configuration fetched and applied.
|
||||
if (await adjustSettingToRemote(host, log, config)) {
|
||||
config = host.services.setting.currentSettings();
|
||||
} else {
|
||||
const canProceed = await adjustSettingToRemote(host, log, config, operation);
|
||||
if (!canProceed) {
|
||||
log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
// log(JSON.stringify(config), LOG_LEVEL_VERBOSE);
|
||||
return canProceed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a scheduled Fetch or Rebuild without changing the selected automatic
|
||||
* synchronisation mode. The persisted Scram switches keep both reflection
|
||||
* directions paused until the existing start-up dialogue resumes them.
|
||||
*/
|
||||
export async function cancelScheduledInitialisation(
|
||||
host: NecessaryServices<"setting" | "appLifecycle", never>,
|
||||
cleanupFlag: () => Promise<void>
|
||||
): Promise<false> {
|
||||
await host.services.setting.applyPartial(
|
||||
{
|
||||
suspendFileWatching: true,
|
||||
suspendParseReplicationResult: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
await cleanupFlag();
|
||||
host.services.appLifecycle.performRestart();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,12 +459,13 @@ export function createRebuildFlagHandler(
|
||||
>(RebuildEverything, { isP2P: isP2PMainRemote(settings) });
|
||||
if (method === "cancelled") {
|
||||
log("Rebuild everything cancelled by user.", LOG_LEVEL_NOTICE);
|
||||
await cleanupFlag();
|
||||
host.services.appLifecycle.performRestart();
|
||||
return false;
|
||||
return await cancelScheduledInitialisation(host, cleanupFlag);
|
||||
}
|
||||
const { extra } = method;
|
||||
await adjustSettingToRemoteIfNeeded(host, log, extra, settings);
|
||||
if (!(await adjustSettingToRemoteIfNeeded(host, log, extra, settings, "rebuild"))) {
|
||||
log("Rebuild initialisation cancelled by user.", LOG_LEVEL_NOTICE);
|
||||
return await cancelScheduledInitialisation(host, cleanupFlag);
|
||||
}
|
||||
return await processVaultInitialisation(host, log, async () => {
|
||||
await host.serviceModules.rebuilder.$rebuildEverything();
|
||||
await cleanupFlag();
|
||||
|
||||
Reference in New Issue
Block a user