diff --git a/docs/settings.md b/docs/settings.md index 1ac07517..6cf14059 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -301,6 +301,18 @@ Setting key: bucketCustomHeaders Custom HTTP headers to include in every request sent to the Object Storage bucket. Specify them in the format `Header-Name: Value`, with each header on a new line. +#### Journal data format + +Setting key: journalFormat + +Existing Object Storage profiles use `opaque-v1` unless Adaptive Journal is selected explicitly. `adaptive-v1` uses an authenticated manifest and immutable Commit Bundles under a separate namespace, with larger Packs stored separately. Changing between formats requires an explicit remote Rebuild; LiveSync does not migrate or read both representations. + +#### Pack retrieval + +Setting key: packReadPolicy + +Adaptive Journal can download a complete immutable Pack (`whole-pack`) or request only the required byte ranges (`range`). Complete Pack retrieval is the portable, throughput-oriented default. Selecting Range requires exact byte-range support from the configured S3-compatible endpoint. The connection test verifies that capability, and synchronisation refuses an unsupported selection before writing. + #### Test Connection #### Apply Settings diff --git a/src/common/messages/combinedMessages.prod.ts b/src/common/messages/combinedMessages.prod.ts index dceb6835..333f7f00 100644 --- a/src/common/messages/combinedMessages.prod.ts +++ b/src/common/messages/combinedMessages.prod.ts @@ -288,6 +288,13 @@ export const allMessages: Readonly { useCustomRequestHandler: false, forcePathStyle: true, bucketCustomHeaders: "", + expectedRepositoryId: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + journalFormat: "adaptive-v1" as const, + packReadPolicy: "range" as const, }; syncActivatedRemoteSettings(target, source); @@ -40,6 +43,9 @@ describe("syncActivatedRemoteSettings", () => { expect(target.bucket).toBe("vault"); expect(target.region).toBe("sz-hq"); expect(target.bucketPrefix).toBe("folder/"); + expect(target.expectedRepositoryId).toBe("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + expect(target.journalFormat).toBe("adaptive-v1"); + expect(target.packReadPolicy).toBe("range"); expect(target.encrypt).toBe(true); }); diff --git a/src/modules/features/SetupManager.unit.spec.ts b/src/modules/features/SetupManager.unit.spec.ts index b9eb45b6..9711517d 100644 --- a/src/modules/features/SetupManager.unit.spec.ts +++ b/src/modules/features/SetupManager.unit.spec.ts @@ -407,6 +407,9 @@ describe("SetupManager", () => { useCustomRequestHandler: false, bucketCustomHeaders: "", forcePathStyle: true, + expectedRepositoryId: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + journalFormat: "adaptive-v1", + packReadPolicy: "range", }) .mockResolvedValueOnce(true); @@ -419,6 +422,9 @@ describe("SetupManager", () => { const activeProfile = current.remoteConfigurations[current.activeConfigurationId]; expect(activeProfile?.name).toBe("S3 notes"); expect(activeProfile?.uri).toContain("sls+s3://key:secret@storage.example"); + expect(activeProfile?.uri).toContain("journalFormat=adaptive-v1"); + expect(activeProfile?.uri).toContain("packReadPolicy=range"); + expect(activeProfile?.uri).toContain("expectedRepositoryId=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); }); it("creates and selects a P2P profile during fresh manual onboarding", async () => { diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte index e31312b9..a9f8aaca 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte @@ -20,10 +20,16 @@ import { copyTo, pickBucketSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { TYPE_CANCELLED, type SetupRemoteBucketResultType } from "./setupDialogTypes"; import { $msg as translateMessage } from "@/common/translation"; + import { normaliseS3JournalSettings } from "./s3JournalSettings"; const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS); - let syncSetting = $state({ ...default_setting }); + let syncSetting = $state({ + ...default_setting, + expectedRepositoryId: default_setting.expectedRepositoryId ?? "", + journalFormat: default_setting.journalFormat ?? "opaque-v1", + packReadPolicy: default_setting.packReadPolicy ?? "whole-pack", + }); type Props = GuestDialogProps; @@ -58,11 +64,10 @@ isEndpointSupplied ); }); + const isAdaptive = $derived(syncSetting.journalFormat === "adaptive-v1"); function generateSetting() { - const connSetting: BucketSyncSetting = { - ...syncSetting, - }; + const connSetting = normaliseS3JournalSettings(syncSetting); const trialSettings: BucketSyncSetting = { ...connSetting, }; @@ -115,8 +120,13 @@ } } function commit() { - const setting = pickBucketSyncSettings(generateSetting()); - setResult(setting); + error = ""; + try { + const setting = pickBucketSyncSettings(generateSetting()); + setResult(setting); + } catch (e) { + error = translateMessage("Invalid Object Storage settings: ${reason}", { reason: `${e}` }); + } } function cancel() { setResult(TYPE_CANCELLED); @@ -220,6 +230,30 @@ + + + + + {translateMessage( + "Adaptive Journal uses immutable objects and a separate remote format. Existing Opaque Journal data is not migrated or read. Rebuild the remote when changing formats." + )} + + {#if isAdaptive} + + + + + {translateMessage( + "Complete Pack reads favour throughput. Range reads can reduce transferred bytes. The connection test verifies exact Range support on this endpoint, and synchronisation refuses an unsupported selection before writing." + )} + + {/if}