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) +})