feat : delete all version selection

This commit is contained in:
马登山
2026-01-09 16:26:52 +08:00
parent 54009a7941
commit 56b63dd9f5
16 changed files with 169 additions and 20 deletions
+117 -11
View File
@@ -85,11 +85,48 @@
"
/>
<object-info ref="infoRef" :bucket-name="bucketName" @refresh-parent="handleObjectDeleted" />
<AlertDialog v-model:open="deleteDialogOpen">
<AlertDialogContent class="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>{{ t('Warning') }}</AlertDialogTitle>
<AlertDialogDescription>
<p>{{ t('Are you sure you want to delete the selected objects?') }}</p>
<div v-if="bucketVersioningEnabled" class="mt-4 flex items-center gap-2">
<Checkbox v-model="deleteAllVersions" />
<span>{{ t('Delete All Versions') }}</span>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel as-child>
<Button variant="outline" class="w-full sm:w-auto text-foreground">
{{ t('Cancel') }}
</Button>
</AlertDialogCancel>
<AlertDialogAction as-child @click.prevent="handleConfirmDelete">
<Button variant="destructive" class="w-full sm:w-auto text-white">
{{ t('Confirm') }}
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { useLocalStorage } from '@vueuse/core'
@@ -110,7 +147,6 @@ import { useTaskManagerStore } from '~/store/tasks'
const { $s3Client } = useNuxtApp()
const { t } = useI18n()
const route = useRoute()
const dialog = useDialog()
const props = defineProps<{ bucket: string; path: string }>()
const uploadPickerVisible = ref(false)
@@ -120,6 +156,10 @@ const searchTerm = ref('')
const loading = ref(false)
const showDeleted = useLocalStorage('object-list-show-deleted', false)
const deleteDialogOpen = ref(false)
const deleteDialogKeys = ref<string[]>([])
const deleteAllVersions = ref(false)
const debounce = (fn: Function, delay: number) => {
let timer: NodeJS.Timeout | null = null
return (...args: any[]) => {
@@ -170,6 +210,8 @@ const nextToken = ref<string | undefined>(undefined)
const infoRef = ref<{ openDrawer: (bucket: string, key: string) => void }>()
const message = useMessage()
const objectApi = useObject({ bucket: bucketName.value })
const { getBucketVersioning } = useBucket({})
const bucketVersioningEnabled = ref(false)
const handleNewObject = (asPrefix: boolean) => {
newObjectAsPrefix.value = asPrefix
@@ -180,6 +222,15 @@ const handleObjectDeleted = () => {
refresh()
}
const loadBucketVersioningStatus = async () => {
try {
const resp = await getBucketVersioning(bucketName.value)
bucketVersioningEnabled.value = resp.Status === 'Enabled'
} catch (error) {
bucketVersioningEnabled.value = false
}
}
const fetchObjects = async (): Promise<ObjectRow[]> => {
loading.value = true
try {
@@ -341,7 +392,7 @@ const columns = computed<ColumnDef<ObjectRow, any>[]>(() => {
{
variant: 'outline',
size: 'sm',
onClick: () => confirmDelete([row.original.Key]),
onClick: () => openDeleteDialog([row.original.Key]),
},
() => [h(Icon, { name: 'ri:delete-bin-5-line', class: 'size-4' }), h('span', t('Delete'))]
),
@@ -371,6 +422,7 @@ const handleAllTasksCompleted = () => {
onMounted(() => {
// 监听所有任务全部完成事件(上传/删除)
taskStore.on('drained', handleAllTasksCompleted)
loadBucketVersioningStatus()
})
onUnmounted(() => {
@@ -526,14 +578,22 @@ const downloadMultiple = async () => {
message.success(t('Download ready'))
}
const confirmDelete = (keys: string[]) => {
dialog.error({
title: t('Warning'),
content: t('Are you sure you want to delete the selected objects?'),
positiveText: t('Confirm'),
negativeText: t('Cancel'),
onPositiveClick: () => handleDelete(keys),
})
const openDeleteDialog = (keys: string[]) => {
deleteDialogKeys.value = keys
deleteAllVersions.value = false
deleteDialogOpen.value = true
}
const handleConfirmDelete = async () => {
const keys = [...deleteDialogKeys.value]
deleteDialogOpen.value = false
if (!keys.length) return
if (bucketVersioningEnabled.value && deleteAllVersions.value) {
await handleDeleteAllVersions(keys)
} else {
await handleDelete(keys)
}
}
const handleDelete = async (keys: string[]) => {
@@ -552,12 +612,58 @@ const handleDelete = async (keys: string[]) => {
}
}
const handleDeleteAllVersions = async (keys: string[]) => {
try {
const targets = await collectKeysForDeletion(keys)
if (!targets.length) {
message.success(t('Delete Success'))
} else {
const items: { key: string; versionId?: string }[] = []
for (const key of targets) {
if (!key) continue
const versions = await objectApi.listObjectVersions(key)
const versionItems = (versions.Versions || [])
.filter(v => v.Key === key && v.VersionId)
.map(v => ({
key,
versionId: v.VersionId!,
}))
const deleteMarkers = (versions.DeleteMarkers || [])
.filter(m => m.Key === key && m.VersionId)
.map(m => ({
key,
versionId: m.VersionId!,
}))
if (versionItems.length === 0 && deleteMarkers.length === 0) {
// 如果没有版本信息,退回到删除当前对象(等价于非版本桶)
items.push({ key })
} else {
items.push(...versionItems, ...deleteMarkers)
}
}
if (!items.length) {
message.success(t('Delete Success'))
} else {
;(taskStore as any).addDeleteVersionedItems(items, bucketName.value)
message.success(t('Delete task created'))
}
}
table.resetRowSelection()
refresh()
} catch (error: any) {
message.error(error?.message || t('Delete Failed'))
}
}
const handleBatchDelete = () => {
if (!checkedKeys.value.length) {
message.warning(t('Please select at least one item'))
return
}
confirmDelete([...checkedKeys.value])
openDeleteDialog([...checkedKeys.value])
}
const formatBytes = (bytes: number) => {
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<Modal v-model="visibleProxy" :title="t('Object Versions')" size="lg" :close-on-backdrop="false">
<Modal v-model="visibleProxy" :title="t('Object Versions')" size="xl" :close-on-backdrop="false">
<div class="space-y-4 z-20">
<DataTable class="pb-4" :table="table" :is-loading="loading" :empty-title="t('No Versions')" />
</div>
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "Keine Aufgaben",
"No Tiers": "Keine Tiers",
"No Versions": "Keine Versionen",
"Delete All Versions": "Alle Versionen löschen",
"No bucket selected": "Kein Bucket ausgewählt",
"No buckets found": "Keine Buckets gefunden",
"No buckets match your search": "Keine Buckets entsprechen Ihrer Suche",
+1
View File
@@ -455,6 +455,7 @@
"No Tasks": "No Tasks",
"No Tiers": "No Tiers",
"No Versions": "No Versions",
"Delete All Versions": "Delete All Versions",
"No bucket selected": "No bucket selected",
"No buckets found": "No buckets found",
"No buckets match your search": "No buckets match your search",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "Sin Tareas",
"No Tiers": "Sin Niveles",
"No Versions": "Sin Versiones",
"Delete All Versions": "Eliminar Todas las Versiones",
"No bucket selected": "No hay bucket seleccionado",
"No buckets found": "No se encontraron buckets",
"No buckets match your search": "No hay buckets que coincidan con su búsqueda",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "Aucune tâche",
"No Tiers": "Aucun niveau",
"No Versions": "Aucune version",
"Delete All Versions": "Supprimer toutes les versions",
"No bucket selected": "Aucun compartiment sélectionné",
"No buckets found": "Aucun compartiment trouvé",
"No buckets match your search": "Aucun compartiment ne correspond à votre recherche",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "Tidak Ada Tugas",
"No Tiers": "Tidak Ada Tier",
"No Versions": "Tidak Ada Versi",
"Delete All Versions": "Hapus Semua Versi",
"No bucket selected": "Tidak ada bucket yang dipilih",
"No buckets found": "Tidak ada bucket ditemukan",
"No buckets match your search": "Tidak ada bucket yang cocok dengan pencarian Anda",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "Nessuna attività",
"No Tiers": "Nessun tier",
"No Versions": "Nessuna versione",
"Delete All Versions": "Elimina tutte le versioni",
"No bucket selected": "Nessun bucket selezionato",
"No buckets found": "Nessun bucket trovato",
"No buckets match your search": "Nessun bucket corrisponde alla tua ricerca",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "タスクなし",
"No Tiers": "ティアなし",
"No Versions": "バージョンなし",
"Delete All Versions": "すべてのバージョンを削除",
"No bucket selected": "バケットが選択されていません",
"No buckets found": "バケットが見つかりません",
"No buckets match your search": "検索に一致するバケットがありません",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "작업 없음",
"No Tiers": "티어 없음",
"No Versions": "버전 없음",
"Delete All Versions": "모든 버전 삭제",
"No bucket selected": "버킷이 선택되지 않음",
"No buckets found": "버킷을 찾을 수 없음",
"No buckets match your search": "검색과 일치하는 버킷이 없음",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "Sem Tarefas",
"No Tiers": "Sem Tiers",
"No Versions": "Sem Versões",
"Delete All Versions": "Excluir Todas as Versões",
"No bucket selected": "Nenhum bucket selecionado",
"No buckets found": "Nenhum bucket encontrado",
"No buckets match your search": "Nenhum bucket corresponde à sua pesquisa",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "Нет задач",
"No Tiers": "Нет уровней",
"No Versions": "Нет версий",
"Delete All Versions": "Удалить все версии",
"No bucket selected": "Бакет не выбран",
"No buckets found": "Бакеты не найдены",
"No buckets match your search": "Нет бакетов, соответствующих вашему поиску",
+1
View File
@@ -454,6 +454,7 @@
"No Tasks": "Görev Yok",
"No Tiers": "No Tiers",
"No Versions": "No Versions",
"Delete All Versions": "Delete All Versions",
"No bucket selected": "No bucket selected",
"No buckets found": "No buckets found",
"No buckets match your search": "No buckets match your search",
+1
View File
@@ -456,6 +456,7 @@
"No Tasks": "暂无任务",
"No Tiers": "无存储层",
"No Versions": "无版本",
"Delete All Versions": "删除所有版本",
"No bucket selected": "未选择存储桶",
"No buckets found": "未找到存储桶",
"No buckets match your search": "没有存储桶匹配您的搜索",
+34 -8
View File
@@ -8,6 +8,11 @@ export interface DeleteTask extends ManagedTask<DeleteStatus> {
key: string
bucketName: string
prefix?: string
/**
* Optional object version id.
* When provided, the delete operation will target the specific version.
*/
versionId?: string
actionLabel: string
displayName: string
subInfo: string
@@ -21,6 +26,11 @@ export interface DeleteTaskConfig {
export interface DeleteTaskHelpers {
handler: TaskHandler<DeleteTask, DeleteStatus>
createTasks: (keys: string[], bucketName: string, prefix?: string) => DeleteTask[]
createVersionedTasks: (
items: { key: string; versionId?: string }[],
bucketName: string,
prefix?: string
) => DeleteTask[]
}
const lifecycle: TaskLifecycleStatus<DeleteStatus> = {
@@ -36,13 +46,14 @@ export const createDeleteTaskHelpers = (s3Client: S3Client, config: DeleteTaskCo
const retryDelay = config.retryDelay ?? 1000
const perform: TaskHandler<DeleteTask, DeleteStatus>['perform'] = async task => {
const { key, bucketName, prefix } = task
const { key, bucketName, prefix, versionId } = task
const abortController = new AbortController()
task.abortController = abortController
const command = new DeleteObjectCommand({
Bucket: bucketName,
Key: (prefix || '') + key,
...(versionId ? { VersionId: versionId } : {}),
})
await s3Client.send(command, { abortSignal: abortController.signal })
@@ -66,24 +77,39 @@ export const createDeleteTaskHelpers = (s3Client: S3Client, config: DeleteTaskCo
retryDelay,
}
const createTasks = (keys: string[], bucketName: string, prefix?: string) =>
keys.map(key => ({
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${key}`,
const createTasksFromItems = (
items: { key: string; versionId?: string }[],
bucketName: string,
prefix?: string
): DeleteTask[] =>
items.map(item => ({
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${item.key}-${item.versionId ?? 'latest'}`,
kind: 'delete' as const,
key,
key: item.key,
versionId: item.versionId,
status: lifecycle.pending,
progress: 0,
bucketName,
prefix: prefix ?? '',
actionLabel: 'Delete',
displayName: key,
subInfo: '',
displayName: item.key,
subInfo: item.versionId ? `Version: ${item.versionId}` : '',
error: undefined,
abortController: undefined,
retryCount: 0,
}))
return { handler, createTasks }
const createTasks = (keys: string[], bucketName: string, prefix?: string) =>
createTasksFromItems(
keys.map(key => ({ key })),
bucketName,
prefix
)
const createVersionedTasks = (items: { key: string; versionId?: string }[], bucketName: string, prefix?: string) =>
createTasksFromItems(items, bucketName, prefix)
return { handler, createTasks, createVersionedTasks }
}
export type {
+5
View File
@@ -83,6 +83,11 @@ export const useTaskManagerStore = defineStore('taskManager', {
this.taskManager.enqueue(newTasks)
},
addDeleteVersionedItems(items: { key: string; versionId?: string }[], bucketName: string, prefix?: string) {
const newTasks = this.deleteHelpers.createVersionedTasks(items, bucketName, prefix)
this.taskManager.enqueue(newTasks)
},
// 通用控制
cancelTask(taskId: string) {
this.taskManager.cancelTask(taskId)