diff --git a/components/profile-modal.html b/components/profile-modal.html
index f94bd2d1..9fb70db7 100644
--- a/components/profile-modal.html
+++ b/components/profile-modal.html
@@ -835,11 +835,21 @@
Enter your Cloudflare R2 credentials. Your Account ID (from dashboard) is used to connect. For Public Access URL, use your custom domain or R2.dev subdomain. Ensure the token has "Object Read & Write" permissions.
+
+
+ Enter your S3-compatible storage details. The Endpoint URL is your provider's API address (e.g., https://s3.amazonaws.com or your MinIO URL).
+
+
Endpoint
+
+
+ Force Path Style
+
+
Region
diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js
index 8855f899..040bab22 100644
--- a/js/ui/profileModalController.js
+++ b/js/ui/profileModalController.js
@@ -1776,6 +1776,9 @@ export class ProfileModalController {
this.storagePrefixWarning = document.getElementById("storagePrefixWarning") || null;
this.storageDefaultInput = document.getElementById("storageDefault") || null;
this.storageR2Helper = document.getElementById("storageR2Helper") || null;
+ this.storageS3Helper = document.getElementById("storageS3Helper") || null;
+ this.storageForcePathStyleInput = document.getElementById("storageForcePathStyle") || null;
+ this.storageForcePathStyleLabel = document.getElementById("storageForcePathStyleLabel") || null;
// Backwards-compatible aliases retained for application code that still
// mirrors DOM references from the controller. These should be removed once
@@ -9042,8 +9045,8 @@ export class ProfileModalController {
fillStorageForm(conn) {
if (!conn) return;
- const { provider, accessKeyId, secretAccessKey, accountId: payloadAccountId, endpoint: payloadEndpoint } = conn;
- const { endpoint, region, bucket, prefix, defaultForUploads, accountId } = conn.meta || {};
+ const { provider, accessKeyId, secretAccessKey, accountId: payloadAccountId, endpoint: payloadEndpoint, forcePathStyle: payloadForcePathStyle } = conn;
+ const { endpoint, region, bucket, prefix, defaultForUploads, accountId, forcePathStyle: metaForcePathStyle } = conn.meta || {};
if (this.storageProviderInput) this.storageProviderInput.value = provider || "cloudflare_r2";
@@ -9058,6 +9061,17 @@ export class ProfileModalController {
if (this.storagePrefixInput) this.storagePrefixInput.value = prefix || "";
if (this.storageDefaultInput) this.storageDefaultInput.checked = !!defaultForUploads;
+ if (this.storageForcePathStyleInput) {
+ if (typeof payloadForcePathStyle === "boolean") {
+ this.storageForcePathStyleInput.checked = payloadForcePathStyle;
+ } else if (typeof metaForcePathStyle === "boolean") {
+ this.storageForcePathStyleInput.checked = metaForcePathStyle;
+ } else {
+ // Default to true for S3 if unspecified
+ this.storageForcePathStyleInput.checked = true;
+ }
+ }
+
this.updateStorageFormVisibility();
this.handlePublicUrlInput();
}
@@ -9098,6 +9112,20 @@ export class ProfileModalController {
if (isR2) this.storageR2Helper.classList.remove("hidden");
else this.storageR2Helper.classList.add("hidden");
}
+
+ if (this.storageS3Helper) {
+ if (!isR2) this.storageS3Helper.classList.remove("hidden");
+ else this.storageS3Helper.classList.add("hidden");
+ }
+
+ if (this.storageForcePathStyleLabel) {
+ if (!isR2) this.storageForcePathStyleLabel.classList.remove("hidden", "flex");
+ else this.storageForcePathStyleLabel.classList.add("hidden");
+
+ if (!isR2) {
+ this.storageForcePathStyleLabel.classList.add("flex");
+ }
+ }
}
async handleUnlockStorage() {
@@ -9195,7 +9223,11 @@ export class ProfileModalController {
}
let publicBaseUrl = "";
- let forcePathStyle = provider === PROVIDERS.GENERIC;
+ // For Generic S3, respect the user's checkbox selection.
+ let forcePathStyle = false;
+ if (provider === PROVIDERS.GENERIC) {
+ forcePathStyle = this.storageForcePathStyleInput?.checked ?? true;
+ }
const payload = {
provider,
@@ -9287,7 +9319,12 @@ export class ProfileModalController {
const accessKeyId = this.storageAccessKeyInput?.value?.trim() || "";
const secretAccessKey = this.storageSecretKeyInput?.value?.trim() || "";
const bucket = this.storageBucketInput?.value?.trim() || "";
- const forcePathStyle = provider === PROVIDERS.GENERIC;
+
+ // Checkbox is only relevant for Generic S3
+ const forcePathStyle = provider === PROVIDERS.GENERIC
+ ? (this.storageForcePathStyleInput?.checked ?? true)
+ : false;
+
const publicBaseUrl = this.storagePrefixInput?.value?.trim() || "";
if (!accessKeyId || !secretAccessKey || !endpointOrAccount) {
@@ -9348,6 +9385,9 @@ export class ProfileModalController {
this.storagePrefixInput.value = "";
this.storageDefaultInput.checked = false;
this.storageProviderInput.value = "cloudflare_r2";
+ if (this.storageForcePathStyleInput) {
+ this.storageForcePathStyleInput.checked = true;
+ }
this.updateStorageFormVisibility();
this.setStorageFormStatus("", "info");
}
diff --git a/tests/ui/storageService.test.mjs b/tests/ui/storageService.test.mjs
index 9ecb00b0..1fbd2a42 100644
--- a/tests/ui/storageService.test.mjs
+++ b/tests/ui/storageService.test.mjs
@@ -134,7 +134,8 @@ describe("StorageService", () => {
region: "us-east-1",
bucket: "my-bucket",
accessKeyId: "minio-key",
- secretAccessKey: "minio-secret"
+ secretAccessKey: "minio-secret",
+ forcePathStyle: true
};
const meta = {
label: "Local Minio",
@@ -151,6 +152,7 @@ describe("StorageService", () => {
assert.strictEqual(retrieved.region, "us-east-1", "Region should be preserved");
assert.strictEqual(retrieved.bucket, "my-bucket", "Bucket should be preserved");
assert.strictEqual(retrieved.accessKeyId, "minio-key");
+ assert.strictEqual(retrieved.forcePathStyle, true, "ForcePathStyle should be preserved");
assert.strictEqual(retrieved.meta.defaultForUploads, true);
});