mirror of
https://github.com/rustfs/console.git
synced 2026-08-28 19:47:21 +08:00
feat: add object rename action (#146)
This commit is contained in:
+119
-2
@@ -13,6 +13,7 @@ import {
|
||||
RiArrowLeftSLine,
|
||||
RiArrowRightSLine,
|
||||
RiEyeLine,
|
||||
RiEdit2Line,
|
||||
} from "@remixicon/react"
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -26,6 +27,15 @@ import {
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { SearchInput } from "@/components/search-input"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
@@ -63,6 +73,12 @@ import {
|
||||
normalizeObjectZipDownloadUrl,
|
||||
type CreateObjectZipDownloadResponse,
|
||||
} from "@/lib/object-zip-download"
|
||||
import {
|
||||
buildRenamedObjectKey,
|
||||
getObjectBaseName,
|
||||
validateObjectRename,
|
||||
type ObjectRenameValidation,
|
||||
} from "@/lib/object-rename"
|
||||
import { TaskStatsButton } from "@/components/tasks/stats-button"
|
||||
import { useAddDeleteKeys, useAddDeleteFolder, useTasks } from "@/contexts/task-context"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
@@ -100,7 +116,7 @@ export function ObjectList({
|
||||
const { t } = useTranslation()
|
||||
const message = useMessage()
|
||||
const api = useApi()
|
||||
const { listObject, getSignedUrl } = useObject(bucket)
|
||||
const { listObject, getSignedUrl, renameObject } = useObject(bucket)
|
||||
const { getBucketVersioning } = useBucket()
|
||||
const { canCapability } = usePermissions()
|
||||
const addDeleteKeys = useAddDeleteKeys()
|
||||
@@ -122,6 +138,10 @@ export function ObjectList({
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
|
||||
const [deleteDialogKeys, setDeleteDialogKeys] = React.useState<string[]>([])
|
||||
const [deleteAllVersions, setDeleteAllVersions] = React.useState(false)
|
||||
const [renameDialogOpen, setRenameDialogOpen] = React.useState(false)
|
||||
const [renameSourceKey, setRenameSourceKey] = React.useState("")
|
||||
const [renameName, setRenameName] = React.useState("")
|
||||
const [renameSubmitting, setRenameSubmitting] = React.useState(false)
|
||||
|
||||
const prefix = decodeURIComponent(path)
|
||||
const resolvedPageSize = normalizeObjectListPageSize(storedPageSize)
|
||||
@@ -333,6 +353,22 @@ export function ObjectList({
|
||||
[message, t, getSignedUrl],
|
||||
)
|
||||
|
||||
const getRenameValidationMessage = React.useCallback(
|
||||
(validation: ObjectRenameValidation) => {
|
||||
switch (validation) {
|
||||
case "empty":
|
||||
return t("Object name is required")
|
||||
case "containsSlash":
|
||||
return t("Object name cannot contain slashes")
|
||||
case "sameName":
|
||||
return t("New object name must be different")
|
||||
}
|
||||
},
|
||||
[t],
|
||||
)
|
||||
|
||||
const renameValidation = renameSourceKey ? validateObjectRename(renameSourceKey, renameName) : "empty"
|
||||
|
||||
const columns: ColumnDef<ObjectRow>[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -409,6 +445,12 @@ export function ObjectList({
|
||||
<span>{t("Download")}</span>
|
||||
</Button>
|
||||
) : null}
|
||||
{canCapability("objects.rename", { bucket, objectKey: row.original.Key, prefix }) ? (
|
||||
<Button variant="outline" size="sm" onClick={() => openRenameDialog(row.original.Key)}>
|
||||
<RiEdit2Line className="size-4" aria-hidden />
|
||||
<span>{t("Rename")}</span>
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{canCapability("objects.delete", { bucket, objectKey: row.original.Key }) ? (
|
||||
@@ -421,7 +463,7 @@ export function ObjectList({
|
||||
),
|
||||
},
|
||||
],
|
||||
[t, displayKey, bucketPath, onOpenInfo, bucket, downloadFile, canCapability, onPreview],
|
||||
[t, displayKey, bucketPath, onOpenInfo, bucket, downloadFile, canCapability, onPreview, prefix],
|
||||
)
|
||||
|
||||
const { table, selectedRowIds } = useDataTable<ObjectRow>({
|
||||
@@ -476,6 +518,50 @@ export function ObjectList({
|
||||
setDeleteDialogOpen(true)
|
||||
}
|
||||
|
||||
const openRenameDialog = (key: string) => {
|
||||
setRenameSourceKey(key)
|
||||
setRenameName(getObjectBaseName(key))
|
||||
setRenameDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleRenameOpenChange = (open: boolean) => {
|
||||
if (renameSubmitting) return
|
||||
setRenameDialogOpen(open)
|
||||
if (!open) {
|
||||
setRenameSourceKey("")
|
||||
setRenameName("")
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmRename = async () => {
|
||||
if (!renameSourceKey) return
|
||||
|
||||
const validation = validateObjectRename(renameSourceKey, renameName)
|
||||
if (validation) {
|
||||
message.warning(getRenameValidationMessage(validation))
|
||||
return
|
||||
}
|
||||
|
||||
const targetKey = buildRenamedObjectKey(renameSourceKey, renameName)
|
||||
const loadingMsg = message.loading(t("Renaming object"), { duration: 0 })
|
||||
setRenameSubmitting(true)
|
||||
|
||||
try {
|
||||
await renameObject(renameSourceKey, targetKey)
|
||||
message.success(t("Object renamed"))
|
||||
table.resetRowSelection()
|
||||
setRenameDialogOpen(false)
|
||||
setRenameSourceKey("")
|
||||
setRenameName("")
|
||||
void fetchObjects({ resetPagination: true })
|
||||
} catch (err) {
|
||||
message.error((err as Error)?.message ?? t("Rename Failed"))
|
||||
} finally {
|
||||
loadingMsg.destroy()
|
||||
setRenameSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
const keys = [...deleteDialogKeys]
|
||||
setDeleteDialogOpen(false)
|
||||
@@ -722,6 +808,37 @@ export function ObjectList({
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<Dialog open={renameDialogOpen} onOpenChange={handleRenameOpenChange} disablePointerDismissal>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("Rename Object")}</DialogTitle>
|
||||
<DialogDescription className="break-all">{renameSourceKey}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
name="object-rename-name"
|
||||
value={renameName}
|
||||
onChange={(e) => setRenameName(e.target.value)}
|
||||
aria-label={t("Object Name")}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
autoFocus
|
||||
/>
|
||||
{renameValidation ? (
|
||||
<p className="text-xs text-destructive">{getRenameValidationMessage(renameValidation)}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" disabled={renameSubmitting} onClick={() => handleRenameOpenChange(false)}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button disabled={Boolean(renameValidation) || renameSubmitting} onClick={handleConfirmRename}>
|
||||
{t("Rename")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback } from "react"
|
||||
import {
|
||||
CopyObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
GetObjectLegalHoldCommand,
|
||||
GetObjectRetentionCommand,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
} from "@aws-sdk/client-s3"
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||
import { useS3 } from "@/contexts/s3-context"
|
||||
import { encodeObjectCopySource } from "@/lib/object-rename"
|
||||
import { decodeS3UrlEncodedObjectList, decodeS3UrlEncodedObjectVersions } from "@/lib/s3-object-encoding"
|
||||
|
||||
function attachIncludeDeletedHeader(command: ListObjectsV2Command) {
|
||||
@@ -88,6 +90,24 @@ export function useObject(bucket: string) {
|
||||
[client, bucket],
|
||||
)
|
||||
|
||||
const renameObject = useCallback(
|
||||
async (sourceKey: string, targetKey: string) => {
|
||||
await client.send(
|
||||
new CopyObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: targetKey,
|
||||
CopySource: encodeObjectCopySource(bucket, sourceKey),
|
||||
IfNoneMatch: "*",
|
||||
MetadataDirective: "COPY",
|
||||
TaggingDirective: "COPY",
|
||||
}),
|
||||
)
|
||||
|
||||
return deleteObject(sourceKey)
|
||||
},
|
||||
[client, bucket, deleteObject],
|
||||
)
|
||||
|
||||
const listObject = useCallback(
|
||||
async (
|
||||
bucketName: string,
|
||||
@@ -278,6 +298,7 @@ export function useObject(bucket: string) {
|
||||
headObject,
|
||||
putObject,
|
||||
deleteObject,
|
||||
renameObject,
|
||||
getSignedUrl: getSignedUrlFn,
|
||||
listObject,
|
||||
mapAllFiles,
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "يجب أن يكون اسم الكائن الجديد مختلفًا",
|
||||
"Object name cannot contain slashes": "لا يمكن أن يحتوي اسم الكائن على شرطات مائلة",
|
||||
"Object name is required": "اسم الكائن مطلوب",
|
||||
"Object renamed": "تمت إعادة تسمية الكائن",
|
||||
"Rename": "إعادة التسمية",
|
||||
"Rename Failed": "فشلت إعادة التسمية",
|
||||
"Rename Object": "إعادة تسمية الكائن",
|
||||
"Renaming object": "جارٍ إعادة تسمية الكائن"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "Der neue Objektname muss anders sein",
|
||||
"Object name cannot contain slashes": "Der Objektname darf keine Schrägstriche enthalten",
|
||||
"Object name is required": "Der Objektname ist erforderlich",
|
||||
"Object renamed": "Objekt wurde umbenannt",
|
||||
"Rename": "Umbenennen",
|
||||
"Rename Failed": "Umbenennen fehlgeschlagen",
|
||||
"Rename Object": "Objekt umbenennen",
|
||||
"Renaming object": "Objekt wird umbenannt"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "New object name must be different",
|
||||
"Object name cannot contain slashes": "Object name cannot contain slashes",
|
||||
"Object name is required": "Object name is required",
|
||||
"Object renamed": "Object renamed",
|
||||
"Rename": "Rename",
|
||||
"Rename Failed": "Rename Failed",
|
||||
"Rename Object": "Rename Object",
|
||||
"Renaming object": "Renaming object"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "El nuevo nombre del objeto debe ser diferente",
|
||||
"Object name cannot contain slashes": "El nombre del objeto no puede contener barras",
|
||||
"Object name is required": "El nombre del objeto es obligatorio",
|
||||
"Object renamed": "Objeto renombrado",
|
||||
"Rename": "Renombrar",
|
||||
"Rename Failed": "Error al renombrar",
|
||||
"Rename Object": "Renombrar objeto",
|
||||
"Renaming object": "Renombrando objeto"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "Le nouveau nom de l’objet doit être différent",
|
||||
"Object name cannot contain slashes": "Le nom de l’objet ne peut pas contenir de barres obliques",
|
||||
"Object name is required": "Le nom de l’objet est obligatoire",
|
||||
"Object renamed": "Objet renommé",
|
||||
"Rename": "Renommer",
|
||||
"Rename Failed": "Échec du renommage",
|
||||
"Rename Object": "Renommer l’objet",
|
||||
"Renaming object": "Renommage de l’objet"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "Nama objek baru harus berbeda",
|
||||
"Object name cannot contain slashes": "Nama objek tidak boleh berisi garis miring",
|
||||
"Object name is required": "Nama objek wajib diisi",
|
||||
"Object renamed": "Objek diganti namanya",
|
||||
"Rename": "Ganti nama",
|
||||
"Rename Failed": "Gagal mengganti nama",
|
||||
"Rename Object": "Ganti nama objek",
|
||||
"Renaming object": "Mengganti nama objek"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "Il nuovo nome dell’oggetto deve essere diverso",
|
||||
"Object name cannot contain slashes": "Il nome dell’oggetto non può contenere barre",
|
||||
"Object name is required": "Il nome dell’oggetto è obbligatorio",
|
||||
"Object renamed": "Oggetto rinominato",
|
||||
"Rename": "Rinomina",
|
||||
"Rename Failed": "Rinomina non riuscita",
|
||||
"Rename Object": "Rinomina oggetto",
|
||||
"Renaming object": "Rinomina oggetto in corso"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "新しいオブジェクト名は異なる必要があります",
|
||||
"Object name cannot contain slashes": "オブジェクト名にスラッシュは使用できません",
|
||||
"Object name is required": "オブジェクト名は必須です",
|
||||
"Object renamed": "オブジェクト名を変更しました",
|
||||
"Rename": "名前を変更",
|
||||
"Rename Failed": "名前の変更に失敗しました",
|
||||
"Rename Object": "オブジェクト名を変更",
|
||||
"Renaming object": "オブジェクト名を変更中"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "새 객체 이름은 달라야 합니다",
|
||||
"Object name cannot contain slashes": "객체 이름에는 슬래시를 포함할 수 없습니다",
|
||||
"Object name is required": "객체 이름은 필수입니다",
|
||||
"Object renamed": "객체 이름이 변경되었습니다",
|
||||
"Rename": "이름 변경",
|
||||
"Rename Failed": "이름 변경 실패",
|
||||
"Rename Object": "객체 이름 변경",
|
||||
"Renaming object": "객체 이름 변경 중"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "O novo nome do objeto deve ser diferente",
|
||||
"Object name cannot contain slashes": "O nome do objeto não pode conter barras",
|
||||
"Object name is required": "O nome do objeto é obrigatório",
|
||||
"Object renamed": "Objeto renomeado",
|
||||
"Rename": "Renomear",
|
||||
"Rename Failed": "Falha ao renomear",
|
||||
"Rename Object": "Renomear objeto",
|
||||
"Renaming object": "Renomeando objeto"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "Новое имя объекта должно отличаться",
|
||||
"Object name cannot contain slashes": "Имя объекта не может содержать косые черты",
|
||||
"Object name is required": "Требуется имя объекта",
|
||||
"Object renamed": "Объект переименован",
|
||||
"Rename": "Переименовать",
|
||||
"Rename Failed": "Не удалось переименовать",
|
||||
"Rename Object": "Переименовать объект",
|
||||
"Renaming object": "Переименование объекта"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "Yeni nesne adı farklı olmalıdır",
|
||||
"Object name cannot contain slashes": "Nesne adı eğik çizgi içeremez",
|
||||
"Object name is required": "Nesne adı gerekli",
|
||||
"Object renamed": "Nesne yeniden adlandırıldı",
|
||||
"Rename": "Yeniden adlandır",
|
||||
"Rename Failed": "Yeniden adlandırma başarısız",
|
||||
"Rename Object": "Nesneyi yeniden adlandır",
|
||||
"Renaming object": "Nesne yeniden adlandırılıyor"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "Selection Locked",
|
||||
"Decommission cleared": "Decommission cleared",
|
||||
"Failed to clear decommission": "Failed to clear decommission",
|
||||
"Clear Decommission": "Clear Decommission"
|
||||
"Clear Decommission": "Clear Decommission",
|
||||
"New object name must be different": "Tên đối tượng mới phải khác",
|
||||
"Object name cannot contain slashes": "Tên đối tượng không được chứa dấu gạch chéo",
|
||||
"Object name is required": "Tên đối tượng là bắt buộc",
|
||||
"Object renamed": "Đã đổi tên đối tượng",
|
||||
"Rename": "Đổi tên",
|
||||
"Rename Failed": "Đổi tên thất bại",
|
||||
"Rename Object": "Đổi tên đối tượng",
|
||||
"Renaming object": "Đang đổi tên đối tượng"
|
||||
}
|
||||
|
||||
@@ -1346,5 +1346,13 @@
|
||||
"Selection Locked": "选择已锁定",
|
||||
"Decommission cleared": "退役记录已清理",
|
||||
"Failed to clear decommission": "清理退役记录失败",
|
||||
"Clear Decommission": "清理退役"
|
||||
"Clear Decommission": "清理退役",
|
||||
"New object name must be different": "新对象名称必须不同",
|
||||
"Object name cannot contain slashes": "对象名称不能包含斜杠",
|
||||
"Object name is required": "对象名称不能为空",
|
||||
"Object renamed": "对象已重命名",
|
||||
"Rename": "重命名",
|
||||
"Rename Failed": "重命名失败",
|
||||
"Rename Object": "重命名对象",
|
||||
"Renaming object": "正在重命名对象"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
export type ObjectRenameValidation = "empty" | "containsSlash" | "sameName"
|
||||
|
||||
export function getObjectBaseName(key: string): string {
|
||||
const normalizedKey = key.replace(/\/+$/, "")
|
||||
const separatorIndex = normalizedKey.lastIndexOf("/")
|
||||
return separatorIndex >= 0 ? normalizedKey.slice(separatorIndex + 1) : normalizedKey
|
||||
}
|
||||
|
||||
export function normalizeObjectRenameName(value: string): string {
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
export function buildRenamedObjectKey(sourceKey: string, newName: string): string {
|
||||
const normalizedName = normalizeObjectRenameName(newName)
|
||||
const normalizedSourceKey = sourceKey.replace(/\/+$/, "")
|
||||
const separatorIndex = normalizedSourceKey.lastIndexOf("/")
|
||||
return separatorIndex >= 0 ? `${normalizedSourceKey.slice(0, separatorIndex + 1)}${normalizedName}` : normalizedName
|
||||
}
|
||||
|
||||
export function validateObjectRename(sourceKey: string, newName: string): ObjectRenameValidation | null {
|
||||
const normalizedName = normalizeObjectRenameName(newName)
|
||||
|
||||
if (!normalizedName) {
|
||||
return "empty"
|
||||
}
|
||||
|
||||
if (normalizedName.includes("/")) {
|
||||
return "containsSlash"
|
||||
}
|
||||
|
||||
if (buildRenamedObjectKey(sourceKey, normalizedName) === sourceKey) {
|
||||
return "sameName"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function encodeObjectCopySource(bucket: string, key: string): string {
|
||||
const encodedKey = key.split("/").map(encodeURIComponent).join("/")
|
||||
return `/${encodeURIComponent(bucket)}/${encodedKey}`
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export type ConsoleCapability =
|
||||
| "objects.view"
|
||||
| "objects.preview"
|
||||
| "objects.download"
|
||||
| "objects.rename"
|
||||
| "objects.delete"
|
||||
| "objects.bulkDelete"
|
||||
| "objects.tag.view"
|
||||
@@ -76,6 +77,10 @@ const CAPABILITY_REQUIREMENTS: Record<ConsoleCapability, CapabilityRequirement[]
|
||||
"objects.view": [{ actions: ["s3:GetObject"], resource: "object" }],
|
||||
"objects.preview": [{ actions: ["s3:GetObject"], resource: "object" }],
|
||||
"objects.download": [{ actions: ["s3:GetObject"], resource: "object" }],
|
||||
"objects.rename": [
|
||||
{ actions: ["s3:GetObject", "s3:DeleteObject"], resource: "object" },
|
||||
{ actions: ["s3:PutObject"], resource: "objectPattern" },
|
||||
],
|
||||
"objects.delete": [{ actions: ["s3:DeleteObject"], resource: "object" }],
|
||||
"objects.bulkDelete": [{ actions: ["s3:DeleteObject"], resource: "objectPattern" }],
|
||||
"objects.tag.view": [{ actions: ["s3:GetObjectTagging"], resource: "object" }],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
|
||||
const loadObjectRename = () => import(new URL("../../lib/object-rename.ts", import.meta.url).href)
|
||||
|
||||
test("getObjectBaseName returns the final object name segment", async () => {
|
||||
const { getObjectBaseName } = await loadObjectRename()
|
||||
|
||||
assert.equal(getObjectBaseName("photos/2026/image.jpg"), "image.jpg")
|
||||
assert.equal(getObjectBaseName("image.jpg"), "image.jpg")
|
||||
})
|
||||
|
||||
test("buildRenamedObjectKey keeps renamed objects in the same prefix", async () => {
|
||||
const { buildRenamedObjectKey } = await loadObjectRename()
|
||||
|
||||
assert.equal(buildRenamedObjectKey("photos/2026/image.jpg", "renamed.jpg"), "photos/2026/renamed.jpg")
|
||||
assert.equal(buildRenamedObjectKey("image.jpg", "renamed.jpg"), "renamed.jpg")
|
||||
})
|
||||
|
||||
test("validateObjectRename rejects empty, nested, and unchanged names", async () => {
|
||||
const { validateObjectRename } = await loadObjectRename()
|
||||
|
||||
assert.equal(validateObjectRename("photos/image.jpg", ""), "empty")
|
||||
assert.equal(validateObjectRename("photos/image.jpg", "nested/image.jpg"), "containsSlash")
|
||||
assert.equal(validateObjectRename("photos/image.jpg", "image.jpg"), "sameName")
|
||||
assert.equal(validateObjectRename("photos/image.jpg", "renamed.jpg"), null)
|
||||
})
|
||||
|
||||
test("encodeObjectCopySource URL-encodes bucket and key path segments", async () => {
|
||||
const { encodeObjectCopySource } = await loadObjectRename()
|
||||
|
||||
assert.equal(encodeObjectCopySource("my.bucket", "a b/c+中文.txt"), "/my.bucket/a%20b/c%2B%E4%B8%AD%E6%96%87.txt")
|
||||
})
|
||||
Reference in New Issue
Block a user