Chore/align bucket and object naming validation with storage api (#37854)

* Align bucket naming validation

* Align folder naming validation

* Remove description
This commit is contained in:
Joshen Lim
2025-08-12 14:01:34 +07:00
committed by GitHub
parent f927998e4e
commit 4418e6df19
4 changed files with 113 additions and 50 deletions

View File

@@ -39,6 +39,7 @@ import {
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { inverseValidBucketNameRegex, validBucketNameRegex } from './CreateBucketModal.utils'
import { convertFromBytes, convertToBytes } from './StorageSettings/StorageSettings.utils'
export interface CreateBucketModalProps {
@@ -46,29 +47,42 @@ export interface CreateBucketModalProps {
onClose: () => void
}
const FormSchema = z.object({
name: z
.string()
.trim()
.min(1, 'Please provide a name for your bucket')
.regex(
/^[a-z0-9.-]+$/,
'The name of the bucket must only contain lowercase letters, numbers, dots, and hyphens'
)
.refine((value) => !value.endsWith(' '), 'The name of the bucket cannot end with a whitespace')
.refine(
(value) => value !== 'public',
'"public" is a reserved name. Please choose another name'
),
type: z.enum(['STANDARD', 'ANALYTICS']).default('STANDARD'),
public: z.boolean().default(false),
has_file_size_limit: z.boolean().default(false),
formatted_size_limit: z.coerce
.number()
.min(0, 'File size upload limit has to be at least 0')
.default(0),
allowed_mime_types: z.string().trim().default(''),
})
const FormSchema = z
.object({
name: z
.string()
.trim()
.min(1, 'Please provide a name for your bucket')
.max(100, 'Bucket name should be below 100 characters')
.refine(
(value) => !value.endsWith(' '),
'The name of the bucket cannot end with a whitespace'
)
.refine(
(value) => value !== 'public',
'"public" is a reserved name. Please choose another name'
),
type: z.enum(['STANDARD', 'ANALYTICS']).default('STANDARD'),
public: z.boolean().default(false),
has_file_size_limit: z.boolean().default(false),
formatted_size_limit: z.coerce
.number()
.min(0, 'File size upload limit has to be at least 0')
.default(0),
allowed_mime_types: z.string().trim().default(''),
})
.superRefine((data, ctx) => {
if (!validBucketNameRegex.test(data.name)) {
const [match] = data.name.match(inverseValidBucketNameRegex) ?? []
ctx.addIssue({
path: ['name'],
code: z.ZodIssueCode.custom,
message: !!match
? `Bucket name cannot contain the "${match}" character`
: 'Bucket name contains an invalid special character',
})
}
})
export type CreateBucketForm = z.infer<typeof FormSchema>
@@ -182,10 +196,9 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
name="name"
render={({ field }) => (
<FormItemLayout
layout="vertical"
label="Name of bucket"
labelOptional="Buckets cannot be renamed once created."
description="Only lowercase letters, numbers, dots, and hyphens"
layout="vertical"
>
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} placeholder="Enter bucket name" />
@@ -194,12 +207,12 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
)}
/>
<div className="flex flex-col gap-y-2 mt-6">
<div className="flex flex-col gap-y-2 mt-2">
<FormField_Shadcn_
control={form.control}
name="type"
render={({ field }) => (
<FormItemLayout>
<FormItemLayout label="Bucket type">
<FormControl_Shadcn_>
<RadioGroupStacked
id="type"

View File

@@ -0,0 +1,15 @@
// [Joshen] These are referenced from the storage API directly here
// https://github.com/supabase/storage/blob/69e4a40799a9d10be0a63a37cbb46d7d9bea0e17/src/storage/limits.ts#L59
// only allow s3 safe characters and characters which require special handling for now
// the slash restriction come from bucket naming rules
// and the rest of the validation rules are based on S3 object key validation.
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
export const validBucketNameRegex = /^(\w|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/
export const inverseValidBucketNameRegex = /[^A-Za-z0-9_!\-.*'() &$@=;:+,?]/
// only allow s3 safe characters and characters which require special handling for now
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
export const validObjectKeyRegex = /^(\w|\/|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/
export const inverseValidObjectKeyRegex = /[^A-Za-z0-9_\/!\-.*'() &$@=;:+,?]/

View File

@@ -2,7 +2,7 @@ import { has } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
import { StorageItem } from '../Storage.types'
import { RowIcon } from './FileExplorerRow'
@@ -13,7 +13,8 @@ export interface FileExplorerRowEditingProps {
}
const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEditingProps) => {
const { renameFile, renameFolder, addNewFolder } = useStorageExplorerStateSnapshot()
const { renameFile, renameFolder, addNewFolder, updateRowStatus } =
useStorageExplorerStateSnapshot()
const inputRef = useRef<any>(null)
const [itemName, setItemName] = useState(item.name)
@@ -28,7 +29,22 @@ const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEdit
await renameFile(item, name, columnIndex)
} else if (has(item, 'id')) {
const itemWithColumnIndex = { ...item, columnIndex }
renameFolder(itemWithColumnIndex, name, columnIndex)
renameFolder({
folder: itemWithColumnIndex,
newName: name,
columnIndex,
onError: () => {
if (event.type === 'blur') {
updateRowStatus({
name: itemWithColumnIndex.name,
status: STORAGE_ROW_STATUS.READY,
columnIndex,
})
} else {
inputRef.current.select()
}
},
})
} else {
addNewFolder({
folderName: name,

View File

@@ -8,6 +8,10 @@ import { proxy, useSnapshot } from 'valtio'
import { createClient, SupabaseClient } from '@supabase/supabase-js'
import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
import { LOCAL_STORAGE_KEYS } from 'common'
import {
inverseValidObjectKeyRegex,
validObjectKeyRegex,
} from 'components/interfaces/Storage/CreateBucketModal.utils'
import {
STORAGE_BUCKET_SORT,
STORAGE_ROW_STATUS,
@@ -260,6 +264,17 @@ function createStorageExplorerState({
.join('/')
},
validateFolderName: (name: string) => {
if (!validObjectKeyRegex.test(name)) {
const [match] = name.match(inverseValidObjectKeyRegex) ?? []
return !!match
? `Folder name cannot contain the "${match}" character`
: 'Folder name contains an invalid special character'
}
return null
},
addNewFolderPlaceholder: (columnIndex: number) => {
const isPrepend = true
const folderName = 'Untitled folder'
@@ -292,22 +307,22 @@ function createStorageExplorerState({
autofix,
columnIndex,
})
if (formattedName === null) {
onError?.()
return
}
if (!/^[a-zA-Z0-9_-\s]*$/.test(formattedName)) {
onError?.()
return toast.error(
'Only alphanumeric characters, hyphens, and underscores are allowed for folder names.'
)
}
if (formattedName.length === 0) {
return state.removeTempRows(columnIndex)
}
const folderNameError = state.validateFolderName(formattedName)
if (folderNameError) {
onError?.()
return toast.error(folderNameError)
}
state.updateFolderAfterEdit({ folderName: formattedName, columnIndex })
const emptyPlaceholderFile = `${formattedName}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`
@@ -602,7 +617,17 @@ function createStorageExplorerState({
}
},
renameFolder: async (folder: StorageItemWithColumn, newName: string, columnIndex: number) => {
renameFolder: async ({
folder,
newName,
columnIndex,
onError,
}: {
folder: StorageItemWithColumn
newName: string
columnIndex: number
onError?: () => void
}) => {
const originalName = folder.name
if (originalName === newName) {
return state.updateRowStatus({
@@ -612,24 +637,18 @@ function createStorageExplorerState({
})
}
const folderNameError = state.validateFolderName(newName)
if (folderNameError) {
onError?.()
return toast.error(folderNameError)
}
const toastId = toast(
<SonnerProgress progress={0} message={`Renaming folder to ${newName}`} />,
{ closeButton: false, position: 'top-right' }
)
try {
/**
* Catch any folder names that contain slash or backslash
*
* this is because slashes are used to denote
* children/parent relationships in bucket
*
* todo: move this to a util file, as createFolder() uses same logic
*/
if (newName.includes('/') || newName.includes('\\')) {
return toast.error(`Folder name cannot contain forward or back slashes.`)
}
state.updateRowStatus({
name: originalName,
status: STORAGE_ROW_STATUS.LOADING,