From da49e4338d4d48eec125b8b7e97b8023c84a3a1b Mon Sep 17 00:00:00 2001 From: cxymds Date: Sun, 5 Jul 2026 17:47:39 +0800 Subject: [PATCH] feat: improve object version management --- components/object/versions.tsx | 77 ++++++++++++++++++++++-- hooks/use-object.ts | 16 +++++ i18n/locales/ar-MA.json | 6 ++ i18n/locales/de-DE.json | 6 ++ i18n/locales/en-US.json | 6 ++ i18n/locales/es-ES.json | 6 ++ i18n/locales/fr-FR.json | 6 ++ i18n/locales/id-ID.json | 6 ++ i18n/locales/it-IT.json | 6 ++ i18n/locales/ja-JP.json | 6 ++ i18n/locales/ko-KR.json | 6 ++ i18n/locales/pt-BR.json | 6 ++ i18n/locales/ru-RU.json | 6 ++ i18n/locales/tr-TR.json | 6 ++ i18n/locales/vi-VN.json | 6 ++ i18n/locales/zh-CN.json | 6 ++ lib/object-rename.ts | 5 +- lib/permission-capabilities.ts | 2 + tests/lib/object-rename.test.ts | 9 +++ tests/lib/object-versions-source.test.js | 28 +++++++++ 20 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 tests/lib/object-versions-source.test.js diff --git a/components/object/versions.tsx b/components/object/versions.tsx index 515134a1..0dbe4069 100644 --- a/components/object/versions.tsx +++ b/components/object/versions.tsx @@ -2,13 +2,15 @@ import * as React from "react" import { useTranslation } from "react-i18next" -import { RiFileCopyLine, RiEyeLine, RiDownloadCloud2Line, RiDeleteBin5Line } from "@remixicon/react" +import { RiFileCopyLine, RiEyeLine, RiDownloadCloud2Line, RiDeleteBin5Line, RiLoopLeftLine } from "@remixicon/react" +import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { DataTable } from "@/components/data-table/data-table" import { useDataTable } from "@/hooks/use-data-table" import { useObject } from "@/hooks/use-object" import { usePermissions } from "@/hooks/use-permissions" +import { useDialog } from "@/lib/feedback/dialog" import { useMessage } from "@/lib/feedback/message" import { copyToClipboard } from "@/lib/clipboard" import { exportFile } from "@/lib/export-file" @@ -24,6 +26,7 @@ interface VersionRow { LastModified?: Date Size?: number Key?: string + IsLatest?: boolean } interface ObjectVersionsProps { @@ -45,7 +48,8 @@ export function ObjectVersions({ }: ObjectVersionsProps) { const { t } = useTranslation() const message = useMessage() - const { listObjectVersions, deleteObject } = useObject(bucketName) + const dialog = useDialog() + const { listObjectVersions, deleteObject, restoreObjectVersion } = useObject(bucketName) const { canCapability } = usePermissions() const client = useS3() @@ -131,10 +135,46 @@ export function ObjectVersions({ [deleteObject, objectKey, message, t, fetchVersions, onRefreshParent], ) + const restoreVersion = React.useCallback( + (row: VersionRow) => { + const versionId = row.VersionId + if (!versionId || row.IsLatest) return + + dialog.warning({ + title: t("Restore Version"), + content: t("This will copy the selected historical version and make it the current object version."), + positiveText: t("Restore"), + negativeText: t("Cancel"), + onPositiveClick: async () => { + try { + await restoreObjectVersion(objectKey, versionId) + message.success(t("Restore Success")) + await fetchVersions() + onRefreshParent() + } catch (err) { + message.error((err as Error)?.message ?? t("Restore Failed")) + return false + } + }, + }) + }, + [dialog, fetchVersions, message, objectKey, onRefreshParent, restoreObjectVersion, t], + ) + + const versionStats = React.useMemo( + () => ({ + count: versions.length, + totalSize: versions.reduce((total, row) => total + (typeof row.Size === "number" ? row.Size : 0), 0), + }), + [versions], + ) + const columns: ColumnDef[] = React.useMemo( () => [ { id: "versionId", + accessorKey: "VersionId", + enableSorting: false, header: () => t("VersionId"), cell: ({ row }) => { const versionId = row.original.VersionId ?? "" @@ -153,22 +193,33 @@ export function ObjectVersions({ > + {row.original.IsLatest ? ( + + {t("Current")} + + ) : null} ) }, + meta: { minWidth: 300 }, }, { id: "lastModified", + accessorFn: (row) => (row.LastModified ? new Date(row.LastModified).getTime() : 0), header: () => t("LastModified"), cell: ({ row }) => (row.original.LastModified ? formatDateTime(row.original.LastModified) : ""), + meta: { width: 200 }, }, { id: "size", + accessorFn: (row) => row.Size ?? 0, header: () => t("Size"), cell: ({ row }) => (typeof row.original.Size === "number" ? formatBytes(row.original.Size) : ""), + meta: { width: 140 }, }, { id: "actions", + enableSorting: false, header: () => t("Action"), cell: ({ row }) => { const objectContext = { @@ -177,13 +228,19 @@ export function ObjectVersions({ } return ( -
+
{canCapability("objects.version.view", objectContext) ? ( ) : null} + {!row.original.IsLatest && canCapability("objects.version.restore", objectContext) ? ( + + ) : null} {canCapability("objects.download", objectContext) ? (
) }, + meta: { minWidth: 360 }, }, ], - [t, onPreview, copyVersionId, downloadVersion, deleteVersion, canCapability, bucketName, objectKey], + [t, onPreview, copyVersionId, restoreVersion, downloadVersion, deleteVersion, canCapability, bucketName, objectKey], ) const { table } = useDataTable({ @@ -216,10 +274,19 @@ export function ObjectVersions({ return ( !open && onClose()} disablePointerDismissal> - + {t("Object Versions")} +
+ + {t("Versions")}: {versionStats.count} + + + {t("Total")} {t("Size")}:{" "} + {formatBytes(versionStats.totalSize)} + +
diff --git a/hooks/use-object.ts b/hooks/use-object.ts index 009a899e..671b6e39 100644 --- a/hooks/use-object.ts +++ b/hooks/use-object.ts @@ -108,6 +108,21 @@ export function useObject(bucket: string) { [client, bucket, deleteObject], ) + const restoreObjectVersion = useCallback( + async (key: string, versionId: string) => { + return client.send( + new CopyObjectCommand({ + Bucket: bucket, + Key: key, + CopySource: encodeObjectCopySource(bucket, key, versionId), + MetadataDirective: "COPY", + TaggingDirective: "COPY", + }), + ) + }, + [client, bucket], + ) + const listObject = useCallback( async ( bucketName: string, @@ -299,6 +314,7 @@ export function useObject(bucket: string) { putObject, deleteObject, renameObject, + restoreObjectVersion, getSignedUrl: getSignedUrlFn, listObject, mapAllFiles, diff --git a/i18n/locales/ar-MA.json b/i18n/locales/ar-MA.json index 6fe29745..5fa76653 100644 --- a/i18n/locales/ar-MA.json +++ b/i18n/locales/ar-MA.json @@ -247,6 +247,7 @@ "Current Site": "الموقع الحالي", "Current User Policy": "سياسة المستخدم الحالية", "Current Version": "الإصدار الحالي", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "حالة الخلفية الحالية للحاويات والمستخدمين والمجموعات والسياسات وقواعد انتهاء دورة الحياة المنسوخة.", "Current site + remote peers": "الموقع الحالي + النظراء البعيدون", "Current user policy": "سياسة المستخدم الحالية", @@ -925,6 +926,10 @@ "Restart KMS": "إعادة تشغيل KMS", "Restart Required": "إعادة التشغيل مطلوبة", "Restore Key": "استعادة المفتاح", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "استئناف", "Resync cancel request sent successfully": "تم إرسال طلب إلغاء إعادة المزامنة بنجاح", "Resync request started successfully": "تم بدء طلب إعادة المزامنة بنجاح", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "لا يمكن التراجع عن هذا الإجراء.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "يقوم هذا الإجراء بإخراج التجمع المحدد من الخدمة، ويجب استخدامه فقط بعد التأكد من اكتمال إعادة التوازن.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "يُستخدم هذا المفتاح كمفتاح SSE الافتراضي للمنصة لكل من SSE-KMS وSSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "يُستخدم هذا المفتاح كافتراضي للمنصة لـ SSE-KMS.", "This link will expire when your session ends or at the specified time.": "ستنتهي صلاحية هذا الرابط عند انتهاء جلستك أو في الوقت المحدد.", "This permanently deletes the key immediately and cannot be undone.": "يحذف المفتاح نهائيًا فورًا ولا يمكن التراجع.", diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index 016662bd..3e4ba7db 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -247,6 +247,7 @@ "Current Site": "Aktueller Standort", "Current User Policy": "Aktuelle Benutzerrichtlinie", "Current Version": "Aktuelle Version", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Aktueller Backend-Status für replizierte Buckets, Benutzer, Gruppen, Richtlinien und Ablaufregeln des Lebenszyklus.", "Current site + remote peers": "Aktuelle Site + entfernte Peers", "Current user policy": "Aktuelle Benutzerrichtlinie", @@ -925,6 +926,10 @@ "Restart KMS": "KMS neu starten", "Restart Required": "Neustart erforderlich", "Restore Key": "Schlüssel wiederherstellen", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Fortsetzen", "Resync cancel request sent successfully": "Anfrage zum Abbrechen der Neusynchronisierung erfolgreich gesendet", "Resync request started successfully": "Neusynchronisierungsanfrage erfolgreich gestartet", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Diese Aktion kann nicht rückgängig gemacht werden.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Diese Aktion legt den ausgewählten Pool still und sollte erst verwendet werden, nachdem der Neuausgleich abgeschlossen ist.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Dieser Schlüssel wird als plattformweiter Standard-SSE-Schlüssel für SSE-KMS und SSE-S3 verwendet.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Dieser Schlüssel ist die Plattform-Standardvorgabe für SSE-KMS.", "This link will expire when your session ends or at the specified time.": "Dieser Link läuft ab, wenn Ihre Sitzung endet oder zum angegebenen Zeitpunkt.", "This permanently deletes the key immediately and cannot be undone.": "Der Schlüssel wird sofort dauerhaft gelöscht und kann nicht rückgängig gemacht werden.", diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index e2862925..8b396308 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -247,6 +247,7 @@ "Current Site": "Current Site", "Current User Policy": "Current User Policy", "Current Version": "Current Version", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.", "Current site + remote peers": "Current site + remote peers", "Current user policy": "Current user policy", @@ -925,6 +926,10 @@ "Restart KMS": "Restart KMS", "Restart Required": "Restart Required", "Restore Key": "Restore Key", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Resume", "Resync cancel request sent successfully": "Resync cancel request sent successfully", "Resync request started successfully": "Resync request started successfully", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "This action cannot be undone.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "This action retires the selected pool and should be used only after verifying rebalance has completed.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "This key is used as the platform default for SSE-KMS.", "This link will expire when your session ends or at the specified time.": "This link will expire when your session ends or at the specified time.", "This permanently deletes the key immediately and cannot be undone.": "This permanently deletes the key immediately and cannot be undone.", diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index 3f1a75eb..d3b2f362 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -247,6 +247,7 @@ "Current Site": "Sitio Actual", "Current User Policy": "Política de Usuario Actual", "Current Version": "Versión Actual", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Estado actual del backend para buckets, usuarios, grupos, políticas y reglas de expiración del ciclo de vida replicados.", "Current site + remote peers": "Sitio actual + pares remotos", "Current user policy": "Política de usuario actual", @@ -925,6 +926,10 @@ "Restart KMS": "Reiniciar KMS", "Restart Required": "Reinicio requerido", "Restore Key": "Restaurar clave", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Reanudar", "Resync cancel request sent successfully": "La solicitud para cancelar la resincronización se envió correctamente", "Resync request started successfully": "La solicitud de resincronización se inició correctamente", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Esta acción no se puede deshacer.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Esta acción retira el pool seleccionado y solo debe usarse tras verificar que el reequilibrio ha finalizado.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Esta clave se usa como la clave SSE predeterminada de la plataforma para SSE-KMS y SSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Esta clave se usa como predeterminada de la plataforma para SSE-KMS.", "This link will expire when your session ends or at the specified time.": "Este enlace caducará cuando finalice su sesión o en la hora especificada.", "This permanently deletes the key immediately and cannot be undone.": "Esto elimina la clave de forma permanente e inmediata y no se puede deshacer.", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index fb33c28d..076c9447 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -247,6 +247,7 @@ "Current Site": "Site actuel", "Current User Policy": "Politique utilisateur actuelle", "Current Version": "Version actuelle", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "État actuel du backend pour les buckets, utilisateurs, groupes, politiques et règles d’expiration du cycle de vie répliqués.", "Current site + remote peers": "Site actuel + pairs distants", "Current user policy": "Politique utilisateur actuelle", @@ -925,6 +926,10 @@ "Restart KMS": "Redémarrer KMS", "Restart Required": "Redémarrage requis", "Restore Key": "Restaurer la clé", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Reprendre", "Resync cancel request sent successfully": "La demande d'annulation de la resynchronisation a été envoyée avec succès", "Resync request started successfully": "La demande de resynchronisation a été lancée avec succès", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Cette action ne peut pas être annulée.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Cette action retire le pool sélectionné et ne doit être utilisée qu’après avoir vérifié que le rééquilibrage est terminé.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Cette clé est utilisée comme clé SSE par défaut de la plateforme pour SSE-KMS et SSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Cette clé est utilisée par défaut sur la plateforme pour SSE-KMS.", "This link will expire when your session ends or at the specified time.": "Ce lien expirera lorsque votre session prendra fin ou à l'heure indiquée.", "This permanently deletes the key immediately and cannot be undone.": "Supprime définitivement la clé immédiatement, sans retour en arrière.", diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index 902b7a7e..a1df89ed 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -247,6 +247,7 @@ "Current Site": "Situs Saat Ini", "Current User Policy": "Kebijakan Pengguna Saat Ini", "Current Version": "Versi Saat Ini", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Status backend saat ini untuk bucket, pengguna, grup, kebijakan, dan aturan kedaluwarsa siklus hidup yang direplikasi.", "Current site + remote peers": "Situs saat ini + peer jarak jauh", "Current user policy": "Kebijakan pengguna saat ini", @@ -925,6 +926,10 @@ "Restart KMS": "Mulai ulang KMS", "Restart Required": "Perlu Restart", "Restore Key": "Pulihkan kunci", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Lanjutkan", "Resync cancel request sent successfully": "Permintaan pembatalan sinkronisasi ulang berhasil dikirim", "Resync request started successfully": "Permintaan sinkronisasi ulang berhasil dimulai", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Tindakan ini tidak dapat dibatalkan.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Tindakan ini memensiunkan pool yang dipilih dan hanya boleh digunakan setelah penyeimbangan ulang selesai.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Kunci ini digunakan sebagai kunci SSE default platform untuk SSE-KMS dan SSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Kunci ini digunakan sebagai default platform untuk SSE-KMS.", "This link will expire when your session ends or at the specified time.": "Tautan ini akan kedaluwarsa saat sesi Anda berakhir atau pada waktu yang ditentukan.", "This permanently deletes the key immediately and cannot be undone.": "Menghapus kunci secara permanen segera dan tidak dapat dibatalkan.", diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index d851c378..14259a1d 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -247,6 +247,7 @@ "Current Site": "Sito corrente", "Current User Policy": "Politica utente corrente", "Current Version": "Versione corrente", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Stato attuale del backend per bucket, utenti, gruppi, policy e regole di scadenza del ciclo di vita replicati.", "Current site + remote peers": "Sito corrente + peer remoti", "Current user policy": "Politica utente corrente", @@ -925,6 +926,10 @@ "Restart KMS": "Riavvia KMS", "Restart Required": "Riavvio richiesto", "Restore Key": "Ripristina chiave", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Riprendi", "Resync cancel request sent successfully": "Richiesta di annullamento della risincronizzazione inviata con successo", "Resync request started successfully": "Richiesta di risincronizzazione avviata con successo", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Questa azione non può essere annullata.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Questa azione ritira il pool selezionato e deve essere usata solo dopo aver verificato il completamento del ribilanciamento.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Questa chiave viene usata come chiave SSE predefinita della piattaforma per SSE-KMS e SSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Questa chiave è il predefinito della piattaforma per SSE-KMS.", "This link will expire when your session ends or at the specified time.": "Questo link scadrà quando la sessione terminerà o all'ora specificata.", "This permanently deletes the key immediately and cannot be undone.": "Elimina definitivamente la chiave immediatamente e non può essere annullato.", diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index e4cc8058..f6feb992 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -247,6 +247,7 @@ "Current Site": "現在のサイト", "Current User Policy": "現在のユーザーポリシー", "Current Version": "現在のバージョン", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "複製対象のバケット、ユーザー、グループ、ポリシー、ライフサイクル期限切れルールの現在のバックエンド状態。", "Current site + remote peers": "現在のサイト + リモートピア", "Current user policy": "現在のユーザーポリシー", @@ -925,6 +926,10 @@ "Restart KMS": "KMS を再起動", "Restart Required": "再起動が必要", "Restore Key": "キーを復元", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "再開", "Resync cancel request sent successfully": "再同期のキャンセルリクエストを正常に送信しました", "Resync request started successfully": "再同期リクエストを正常に開始しました", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "この操作は元に戻せません。", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "この操作は選択したプールを廃止します。リバランス完了を確認した後でのみ実行してください。", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "このキーは、SSE-KMS および SSE-S3 用のプラットフォーム既定の SSE キーとして使用されます。", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "このキーはプラットフォームの SSE-KMS デフォルトとして使用されます。", "This link will expire when your session ends or at the specified time.": "このリンクはセッション終了時、または指定した時刻に期限切れになります。", "This permanently deletes the key immediately and cannot be undone.": "キーをすぐに完全削除します。元に戻せません。", diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index 71b01408..f6eb2705 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -247,6 +247,7 @@ "Current Site": "현재 사이트", "Current User Policy": "현재 사용자 정책", "Current Version": "현재 버전", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "복제된 버킷, 사용자, 그룹, 정책 및 수명 주기 만료 규칙의 현재 백엔드 상태입니다.", "Current site + remote peers": "현재 사이트 + 원격 피어", "Current user policy": "현재 사용자 정책", @@ -925,6 +926,10 @@ "Restart KMS": "KMS 재시작", "Restart Required": "재시작 필요", "Restore Key": "키 복원", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "재개", "Resync cancel request sent successfully": "재동기화 취소 요청을 성공적으로 전송했습니다", "Resync request started successfully": "재동기화 요청을 성공적으로 시작했습니다", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "이 작업은 실행 취소할 수 없습니다.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "이 작업은 선택한 풀을 폐기합니다. 리밸런스가 완료된 것을 확인한 후에만 사용하세요.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "이 키는 SSE-KMS 및 SSE-S3에 대한 플랫폼 기본 SSE 키로 사용됩니다.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "이 키는 플랫폼의 SSE-KMS 기본값으로 사용됩니다.", "This link will expire when your session ends or at the specified time.": "이 링크는 세션이 종료되거나 지정한 시각에 만료됩니다.", "This permanently deletes the key immediately and cannot be undone.": "키를 즉시 영구 삭제하며 되돌릴 수 없습니다.", diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index d1e84f6a..a7dd4214 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -247,6 +247,7 @@ "Current Site": "Site Atual", "Current User Policy": "Política de Usuário Atual", "Current Version": "Versão Atual", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Status atual do backend para buckets, usuários, grupos, políticas e regras de expiração do ciclo de vida replicados.", "Current site + remote peers": "Site atual + pares remotos", "Current user policy": "Política de usuário atual", @@ -925,6 +926,10 @@ "Restart KMS": "Reiniciar KMS", "Restart Required": "Reinicialização necessária", "Restore Key": "Restaurar chave", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Retomar", "Resync cancel request sent successfully": "Solicitação de cancelamento da resincronização enviada com sucesso", "Resync request started successfully": "Solicitação de resincronização iniciada com sucesso", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Esta ação não pode ser desfeita.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Esta ação aposenta o pool selecionado e só deve ser usada após verificar que o rebalanceamento foi concluído.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Esta chave é usada como a chave SSE padrão da plataforma para SSE-KMS e SSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Esta chave é usada como padrão da plataforma para SSE-KMS.", "This link will expire when your session ends or at the specified time.": "Este link expirará quando sua sessão terminar ou no horário especificado.", "This permanently deletes the key immediately and cannot be undone.": "Exclui a chave permanentemente de imediato e não pode ser desfeito.", diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index c102c674..db9c0018 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -247,6 +247,7 @@ "Current Site": "Текущий сайт", "Current User Policy": "Текущая политика пользователя", "Current Version": "Текущая версия", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Текущий статус backend для реплицированных бакетов, пользователей, групп, политик и правил истечения жизненного цикла.", "Current site + remote peers": "Текущий сайт + удаленные пиры", "Current user policy": "Текущая политика пользователя", @@ -925,6 +926,10 @@ "Restart KMS": "Перезапустить KMS", "Restart Required": "Требуется перезапуск", "Restore Key": "Восстановить ключ", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Возобновить", "Resync cancel request sent successfully": "Запрос на отмену повторной синхронизации успешно отправлен", "Resync request started successfully": "Запрос повторной синхронизации успешно запущен", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Это действие нельзя отменить.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Это действие выводит выбранный пул из эксплуатации и должно использоваться только после завершения перебалансировки.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Этот ключ используется как ключ SSE по умолчанию платформы для SSE-KMS и SSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Этот ключ используется по умолчанию платформы для SSE-KMS.", "This link will expire when your session ends or at the specified time.": "Эта ссылка истечет после завершения вашей сессии или в указанное время.", "This permanently deletes the key immediately and cannot be undone.": "Немедленно и безвозвратно удаляет ключ.", diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index 268f7d38..d6993f0c 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -247,6 +247,7 @@ "Current Site": "Mevcut Site", "Current User Policy": "Mevcut Kullanıcı Politikası", "Current Version": "Mevcut Sürüm", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Çoğaltılan bucket'lar, kullanıcılar, gruplar, politikalar ve yaşam döngüsü sona erme kuralları için geçerli backend durumu.", "Current site + remote peers": "Geçerli site + uzak eşler", "Current user policy": "Mevcut kullanıcı politikası", @@ -925,6 +926,10 @@ "Restart KMS": "KMS’yi yeniden başlat", "Restart Required": "Yeniden Başlatma Gerekli", "Restore Key": "Anahtarı geri yükle", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Devam Et", "Resync cancel request sent successfully": "Yeniden senkronizasyon iptal isteği başarıyla gönderildi", "Resync request started successfully": "Yeniden senkronizasyon isteği başarıyla başlatıldı", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Bu işlem geri alınamaz.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Bu işlem seçilen pool’u devreden çıkarır ve yalnızca yeniden dengeleme tamamlandıktan sonra kullanılmalıdır.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Bu anahtar, SSE-KMS ve SSE-S3 için platformun varsayılan SSE anahtarı olarak kullanılır.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Bu anahtar platformun SSE-KMS varsayılanıdır.", "This link will expire when your session ends or at the specified time.": "Bu bağlantı oturumunuz sona erdiğinde veya belirtilen saatte süresi dolacaktır.", "This permanently deletes the key immediately and cannot be undone.": "Anahtarı hemen kalıcı olarak siler ve geri alınamaz.", diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index 343ee21a..f0cd7297 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -247,6 +247,7 @@ "Current Site": "Cụm máy chủ hiện tại", "Current User Policy": "Chính sách người dùng hiện tại", "Current Version": "Phiên bản hiện tại", + "Current": "Current", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Trạng thái backend hiện tại của các bucket, người dùng, nhóm, chính sách và quy tắc hết hạn vòng đời được sao chép.", "Current site + remote peers": "Site hiện tại + các peer từ xa", "Current user policy": "Chính sách người dùng hiện tại", @@ -925,6 +926,10 @@ "Restart KMS": "Khởi động lại KMS", "Restart Required": "Cần khởi động lại", "Restore Key": "Khôi phục khóa", + "Restore": "Restore", + "Restore Failed": "Restore Failed", + "Restore Success": "Restore Success", + "Restore Version": "Restore Version", "Resume": "Tiếp tục", "Resync cancel request sent successfully": "Đã gửi yêu cầu hủy đồng bộ lại thành công", "Resync request started successfully": "Đã bắt đầu yêu cầu đồng bộ lại thành công", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "Hành động này không thể hoàn tác.", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "Hành động này ngừng sử dụng pool đã chọn và chỉ nên dùng sau khi xác nhận cân bằng lại đã hoàn tất.", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Khóa này được dùng làm khóa SSE mặc định của nền tảng cho SSE-KMS và SSE-S3.", + "This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.", "This key is used as the platform default for SSE-KMS.": "Khóa này là mặc định nền tảng cho SSE-KMS.", "This link will expire when your session ends or at the specified time.": "Liên kết này sẽ hết hạn khi phiên làm việc của bạn kết thúc hoặc vào thời gian đã định.", "This permanently deletes the key immediately and cannot be undone.": "Xóa khóa vĩnh viễn ngay lập tức và không thể hoàn tác.", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 97dac869..f1323e5b 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -247,6 +247,7 @@ "Current Site": "当前站点", "Current User Policy": "当前用户策略", "Current Version": "当前版本", + "Current": "当前", "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "复制存储桶、用户、用户组、策略和生命周期过期规则的当前后端状态。", "Current site + remote peers": "当前站点 + 远程对等站点", "Current user policy": "当前用户策略", @@ -925,6 +926,10 @@ "Restart KMS": "重启 KMS", "Restart Required": "需要重启", "Restore Key": "恢复密钥", + "Restore": "恢复", + "Restore Failed": "恢复失败", + "Restore Success": "恢复成功", + "Restore Version": "恢复版本", "Resume": "恢复", "Resync cancel request sent successfully": "取消重同步请求已成功发送", "Resync request started successfully": "重同步请求已成功启动", @@ -1133,6 +1138,7 @@ "This action cannot be undone.": "此操作无法撤销。", "This action retires the selected pool and should be used only after verifying rebalance has completed.": "此操作会退役所选存储池,仅应在确认再平衡已完成后执行。", "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "此密钥将作为平台默认 SSE 密钥,用于 SSE-KMS 和 SSE-S3。", + "This will copy the selected historical version and make it the current object version.": "这会复制选中的历史版本,并将其设为当前对象版本。", "This key is used as the platform default for SSE-KMS.": "该密钥将作为平台默认的 SSE-KMS 密钥。", "This link will expire when your session ends or at the specified time.": "此链接将会在您的登录会话结束或达到指定时间后失效。", "This permanently deletes the key immediately and cannot be undone.": "这会立即永久删除该密钥,且无法撤销。", diff --git a/lib/object-rename.ts b/lib/object-rename.ts index 7c7eed2e..a2dc10dd 100644 --- a/lib/object-rename.ts +++ b/lib/object-rename.ts @@ -35,7 +35,8 @@ export function validateObjectRename(sourceKey: string, newName: string): Object return null } -export function encodeObjectCopySource(bucket: string, key: string): string { +export function encodeObjectCopySource(bucket: string, key: string, versionId?: string): string { const encodedKey = key.split("/").map(encodeURIComponent).join("/") - return `/${encodeURIComponent(bucket)}/${encodedKey}` + const source = `/${encodeURIComponent(bucket)}/${encodedKey}` + return versionId ? `${source}?versionId=${encodeURIComponent(versionId)}` : source } diff --git a/lib/permission-capabilities.ts b/lib/permission-capabilities.ts index b4673967..f935048f 100644 --- a/lib/permission-capabilities.ts +++ b/lib/permission-capabilities.ts @@ -34,6 +34,7 @@ export type ConsoleCapability = | "objects.legalHold.edit" | "objects.retention.edit" | "objects.version.view" + | "objects.version.restore" | "objects.share" | "accessKeys.create" | "accessKeys.edit" @@ -88,6 +89,7 @@ const CAPABILITY_REQUIREMENTS: Record { + const { encodeObjectCopySource } = await loadObjectRename() + + assert.equal( + encodeObjectCopySource("my.bucket", "a b/c+中文.txt", "2026/07/04 version+1"), + "/my.bucket/a%20b/c%2B%E4%B8%AD%E6%96%87.txt?versionId=2026%2F07%2F04%20version%2B1", + ) +}) diff --git a/tests/lib/object-versions-source.test.js b/tests/lib/object-versions-source.test.js new file mode 100644 index 00000000..fb0bd616 --- /dev/null +++ b/tests/lib/object-versions-source.test.js @@ -0,0 +1,28 @@ +import test from "node:test" +import assert from "node:assert/strict" +import fs from "node:fs" + +test("object versions restore copies a source version back to the current object key", () => { + const hookSource = fs.readFileSync("hooks/use-object.ts", "utf8") + const componentSource = fs.readFileSync("components/object/versions.tsx", "utf8") + const permissionSource = fs.readFileSync("lib/permission-capabilities.ts", "utf8") + + assert.match(hookSource, /const restoreObjectVersion = useCallback/) + assert.match(hookSource, /CopySource: encodeObjectCopySource\(bucket, key, versionId\)/) + assert.match(hookSource, /MetadataDirective: "COPY"/) + assert.match(hookSource, /TaggingDirective: "COPY"/) + assert.match(componentSource, /restoreObjectVersion\(objectKey, versionId\)/) + assert.match(componentSource, /canCapability\("objects\.version\.restore", objectContext\)/) + assert.match(componentSource, /!row\.original\.IsLatest/) + assert.match(permissionSource, /"objects\.version\.restore": \[\{ actions: \["s3:GetObject", "s3:PutObject"\]/) +}) + +test("object versions dialog is wide, sortable by date and size, and shows summary statistics", () => { + const source = fs.readFileSync("components/object/versions.tsx", "utf8") + + assert.match(source, /2xl:w-\[80vw\]/) + assert.match(source, /count: versions\.length/) + assert.match(source, /totalSize: versions\.reduce/) + assert.match(source, /accessorFn: \(row\) => \(row\.LastModified \? new Date\(row\.LastModified\)\.getTime\(\) : 0\)/) + assert.match(source, /accessorFn: \(row\) => row\.Size \?\? 0/) +})