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:
@@ -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"
|
||||
|
||||
@@ -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_\/!\-.*'() &$@=;:+,?]/
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user