diff --git a/components/buckets/new-form.tsx b/components/buckets/new-form.tsx
index d97bf5f..f5bb6a7 100644
--- a/components/buckets/new-form.tsx
+++ b/components/buckets/new-form.tsx
@@ -4,13 +4,14 @@ import * as React from "react"
import { useTranslation } from "react-i18next"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
-import { Field, FieldContent, FieldLabel } from "@/components/ui/field"
+import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useMessage } from "@/lib/feedback/message"
import { useBucket } from "@/hooks/use-bucket"
import { usePermissions } from "@/hooks/use-permissions"
+import { getOptionalBucketNameError } from "@/lib/bucket-name"
import { getBytes } from "@/lib/functions"
import { cn } from "@/lib/utils"
@@ -48,9 +49,9 @@ export function BucketNewForm({ show, onShowChange }: BucketNewFormProps) {
const [creating, setCreating] = React.useState(false)
const trimmedBucketName = objectKey.trim()
- const showNameError = trimmedBucketName.length > 0 && (trimmedBucketName.length < 3 || trimmedBucketName.length > 63)
+ const nameError = getOptionalBucketNameError(objectKey)
const parsedRetentionPeriod = Math.max(1, Number.parseInt(retentionPeriod, 10) || 1)
- const isSubmitDisabled = creating || trimmedBucketName.length < 3 || trimmedBucketName.length > 63
+ const isSubmitDisabled = creating || !trimmedBucketName || !!nameError
const canCreateBucket = canCapability("bucket.create")
const bucketContext = trimmedBucketName ? { bucket: trimmedBucketName } : {}
const canEditVersioning = canCapability("bucket.versioning.edit", bucketContext)
@@ -85,6 +86,10 @@ export function BucketNewForm({ show, onShowChange }: BucketNewFormProps) {
const handleCreateBucket = async () => {
if (!canCreateBucket || isSubmitDisabled) return
+ if (nameError) {
+ message.error(t(nameError))
+ return
+ }
const bucketName = trimmedBucketName
setCreating(true)
@@ -161,9 +166,10 @@ export function BucketNewForm({ show, onShowChange }: BucketNewFormProps) {
value={objectKey}
onChange={(e) => setObjectKey(e.target.value)}
autoComplete="off"
- className={cn("w-full", showNameError && "border-destructive focus-visible:ring-destructive")}
+ className={cn("w-full", nameError && "border-destructive focus-visible:ring-destructive")}
/>
+ {nameError && {t(nameError)}}
diff --git a/hooks/use-bucket.ts b/hooks/use-bucket.ts
index 049e7da..5e45cc1 100644
--- a/hooks/use-bucket.ts
+++ b/hooks/use-bucket.ts
@@ -40,6 +40,7 @@ import {
} from "@aws-sdk/client-s3"
import { useS3 } from "@/contexts/s3-context"
import { useApi } from "@/contexts/api-context"
+import { getBucketNameError } from "@/lib/bucket-name"
const BUCKETS_CACHE_DURATION = 10000
let listBucketsCache: ListBucketsCommandOutput | null = null
@@ -97,7 +98,17 @@ export function useBucket() {
const createBucket = useCallback(
async (params: { Bucket: string; ObjectLockEnabledForBucket?: boolean }) => {
- const result = await client.send(new CreateBucketCommand(params))
+ const nameError = getBucketNameError(params.Bucket)
+ if (nameError) {
+ throw new Error(nameError)
+ }
+
+ const result = await client.send(
+ new CreateBucketCommand({
+ ...params,
+ Bucket: params.Bucket.trim(),
+ }),
+ )
invalidateBucketsCache()
return result
},
diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json
index e49609a..529c6ff 100644
--- a/i18n/locales/en-US.json
+++ b/i18n/locales/en-US.json
@@ -92,6 +92,8 @@
"Bucket Policy": "Bucket Policy",
"Bucket Quota": "Bucket Quota",
"Bucket Quota Exceeded": "Bucket Quota Exceeded",
+ "Bucket names must be 3-63 characters long": "Bucket names must be 3-63 characters long",
+ "Bucket names must not contain uppercase letters": "Bucket names must not contain uppercase letters",
"Quota Size": "Quota Size",
"Bucket Quota Insufficient": "Bucket Quota Insufficient",
"Quota Warning Content": "The total size of the selected files is {total}, which exceeds the remaining bucket quota {remaining}. Do you want to continue?",
diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json
index 328c66c..a07cd50 100644
--- a/i18n/locales/zh-CN.json
+++ b/i18n/locales/zh-CN.json
@@ -97,6 +97,8 @@
"Bucket Quota": "存储桶配额",
"Bucket Quota Exceeded": "存储桶配额已超出",
"Bucket Quota Insufficient": "存储桶配额不足",
+ "Bucket names must be 3-63 characters long": "存储桶名称长度必须为 3-63 个字符",
+ "Bucket names must not contain uppercase letters": "存储桶名称不能包含大写字母",
"Set Bucket CORS": "设置存储桶跨域配置",
"Bucket Replication": "存储桶复制",
"Bucket Setting": "存储桶设置",
diff --git a/lib/bucket-name.ts b/lib/bucket-name.ts
new file mode 100644
index 0000000..f2b1bf7
--- /dev/null
+++ b/lib/bucket-name.ts
@@ -0,0 +1,29 @@
+const BUCKET_NAME_MIN_LENGTH = 3
+const BUCKET_NAME_MAX_LENGTH = 63
+const UPPERCASE_BUCKET_NAME_PATTERN = /[A-Z]/
+
+export function getBucketNameError(name: string): string | null {
+ const trimmedName = name.trim()
+
+ if (trimmedName.length < BUCKET_NAME_MIN_LENGTH || trimmedName.length > BUCKET_NAME_MAX_LENGTH) {
+ return "Bucket names must be 3-63 characters long"
+ }
+
+ if (UPPERCASE_BUCKET_NAME_PATTERN.test(trimmedName)) {
+ return "Bucket names must not contain uppercase letters"
+ }
+
+ return null
+}
+
+export function getOptionalBucketNameError(name: string): string | null {
+ if (!name.trim()) {
+ return null
+ }
+
+ return getBucketNameError(name)
+}
+
+export function isBucketNameValid(name: string): boolean {
+ return getBucketNameError(name) === null
+}
diff --git a/tests/lib/bucket-name.test.ts b/tests/lib/bucket-name.test.ts
new file mode 100644
index 0000000..e839997
--- /dev/null
+++ b/tests/lib/bucket-name.test.ts
@@ -0,0 +1,19 @@
+import test from "node:test"
+import assert from "node:assert/strict"
+import { getBucketNameError, getOptionalBucketNameError, isBucketNameValid } from "../../lib/bucket-name"
+
+test("isBucketNameValid accepts lowercase bucket names", () => {
+ assert.equal(isBucketNameValid("bucket-demo"), true)
+})
+
+test("getBucketNameError rejects uppercase letters", () => {
+ assert.equal(getBucketNameError("Bucket-Demo"), "Bucket names must not contain uppercase letters")
+})
+
+test("getBucketNameError rejects names shorter than 3 characters", () => {
+ assert.equal(getBucketNameError("ab"), "Bucket names must be 3-63 characters long")
+})
+
+test("getOptionalBucketNameError ignores empty input", () => {
+ assert.equal(getOptionalBucketNameError(" "), null)
+})