diff --git a/components/object/list.tsx b/components/object/list.tsx index 24239eec..8e1dcc25 100644 --- a/components/object/list.tsx +++ b/components/object/list.tsx @@ -67,7 +67,6 @@ import { downloadUrl } from "@/lib/export-file" import { getAttachmentContentDisposition } from "@/lib/content-disposition" import { formatBytes, formatDateTime } from "@/lib/functions" import { cn } from "@/lib/utils" -import { PrefixSummaryLimitError } from "@/lib/prefix-summary" import { normalizeDateToIso } from "@/lib/safe-date" import { buildBucketPath } from "@/lib/bucket-path" import { @@ -116,7 +115,6 @@ interface ObjectRow { type: "prefix" | "object" Size: number LastModified: string - summary?: { size: number; lastModified?: Date } deleted: boolean } @@ -170,7 +168,7 @@ export function ObjectList({ const { t } = useTranslation() const message = useMessage() const api = useApi() - const { listObject, calculatePrefixSummary, getSignedUrl, renameObject } = useObject(bucket) + const { listObject, getSignedUrl, renameObject } = useObject(bucket) const { getBucketVersioning } = useBucket() const { canCapability } = usePermissions() const addDeleteKeys = useAddDeleteKeys() @@ -187,14 +185,6 @@ export function ObjectList({ OBJECT_LIST_DEFAULT_PAGE_SIZE, ) const deletedView = resolveObjectListDeletedView(storedDeletedView) - const [prefixSummaries, setPrefixSummaries] = React.useState>( - {}, - ) - const [prefixScan, setPrefixScan] = React.useState<{ - key: string - state: "loading" | "failed" | "cancelled" | "limit" | "success" - } | null>(null) - const prefixScanRef = React.useRef(null) const [loading, setLoading] = React.useState(false) const [listError, setListError] = React.useState(null) const [loadMoreError, setLoadMoreError] = React.useState(false) @@ -384,53 +374,7 @@ export function ObjectList({ [bucket, prefix, resolvedPageSize, includeDeleted, listObject, message, t], ) - const scanPrefix = React.useCallback( - async (key: string) => { - if (prefixScanRef.current) return - const controller = new AbortController() - prefixScanRef.current = controller - setPrefixSummaries((current) => { - const next = { ...current } - delete next[key] - return next - }) - setPrefixScan({ key, state: "loading" }) - try { - const summary = await calculatePrefixSummary(key, controller.signal) - if (prefixScanRef.current !== controller) return - controller.signal.throwIfAborted() - setPrefixSummaries((current) => ({ ...current, [key]: summary })) - setPrefixScan({ key, state: "success" }) - } catch (error) { - if (prefixScanRef.current !== controller) return - setPrefixScan({ - key, - state: controller.signal.aborted - ? "cancelled" - : error instanceof PrefixSummaryLimitError - ? "limit" - : "failed", - }) - } finally { - if (prefixScanRef.current === controller) prefixScanRef.current = null - } - }, - [calculatePrefixSummary], - ) - - React.useEffect( - () => () => { - prefixScanRef.current?.abort() - prefixScanRef.current = null - }, - [listScope], - ) - const resetAndFetchObjects = React.useCallback(() => { - prefixScanRef.current?.abort() - prefixScanRef.current = null - setPrefixScan(null) - setPrefixSummaries({}) setNextToken(undefined) setData([]) void fetchObjects({ resetPagination: true }) @@ -639,29 +583,8 @@ export function ObjectList({ { id: "size", header: () => t("Size"), - accessorFn: (row) => (row.type === "object" ? row.Size : (row.summary?.size ?? -1)), - cell: ({ row }) => { - if (row.original.type === "object") return formatBytes(row.original.Size) - const key = row.original.Key - const summary = row.original.summary - const state = prefixScan?.key === key ? prefixScan.state : null - return ( -
- {summary ? {formatBytes(summary.size)} : -} - {state !== "loading" ? ( - - ) : null} -
- ) - }, + accessorFn: (row) => (row.type === "object" ? row.Size : -1), + cell: ({ row }) => (row.original.type === "object" ? formatBytes(row.original.Size) : "-"), }, { id: "lastModified", @@ -673,14 +596,8 @@ export function ObjectList({ ) : null} ), - accessorFn: (row) => { - const date = row.type === "prefix" ? row.summary?.lastModified : row.LastModified - return date ? new Date(date).getTime() : -1 - }, - cell: ({ row }) => { - const date = row.original.type === "prefix" ? row.original.summary?.lastModified : row.original.LastModified - return date ? formatDateTime(date) : "-" - }, + accessorFn: (row) => (row.type === "prefix" || !row.LastModified ? "" : new Date(row.LastModified).getTime()), + cell: ({ row }) => (row.original.LastModified ? formatDateTime(row.original.LastModified) : "-"), }, { id: "actions", @@ -811,20 +728,11 @@ export function ObjectList({ nextToken, setRowActionTrigger, getRowActionTrigger, - prefixScan, - scanPrefix, ], ) - const tableRows = React.useMemo(() => { - if (Object.keys(prefixSummaries).length === 0) return filteredData - return filteredData.map((row) => { - const summary = row.type === "prefix" ? prefixSummaries[row.Key] : undefined - return summary ? { ...row, summary } : row - }) - }, [filteredData, prefixSummaries]) const { table, selectedRowIds } = useDataTable({ - data: tableRows, + data: filteredData, columns, getRowId: (row) => row.Key, enableRowSelection: true, @@ -1057,65 +965,6 @@ export function ObjectList({ {heading ??

{bucket}

} - {data.some((row) => row.type === "prefix") ? ( -

- {t( - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.", - )} -

- ) : null} - {prefixScan ? ( -
-
-
- - {prefixScan.state === "loading" - ? t("Calculating folder statistics...") - : prefixScan.state === "cancelled" - ? t("Calculation cancelled.") - : prefixScan.state === "limit" - ? t("Folder exceeds the scan limit.") - : prefixScan.state === "success" - ? t("Complete") - : t("Could not calculate folder statistics.")} - - {prefixScan.key} -
- {prefixScan.state === "success" && prefixSummaries[prefixScan.key] ? ( -
- - {t("Size")}: {formatBytes(prefixSummaries[prefixScan.key].size)} - - - {t("Last Modified")}:{" "} - {prefixSummaries[prefixScan.key].lastModified - ? formatDateTime(prefixSummaries[prefixScan.key].lastModified) - : "-"} - -
- ) : null} -
- {prefixScan.state === "loading" ? ( - - ) : ( - - )} -
- ) : null} fetchPrefixSummary(client, bucket, prefix, signal), - [client, bucket], - ) - const mapAllFiles = useCallback( async (bucketName: string, prefix: string, callback: (fileKey: string) => void) => { let isTruncated = true @@ -346,7 +340,6 @@ export function useObject(bucket: string) { restoreObjectVersion, getSignedUrl: getSignedUrlFn, listObject, - calculatePrefixSummary, mapAllFiles, getObjectInfo, getObjectTags, diff --git a/i18n/locales/ar-MA.json b/i18n/locales/ar-MA.json index 0b720c8c..72696aae 100644 --- a/i18n/locales/ar-MA.json +++ b/i18n/locales/ar-MA.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "اكتمل مسح التخزين المؤقت مع تحذيرات", "Cache cleared successfully": "تم مسح التخزين المؤقت بنجاح", "Cache time-to-live in seconds, default: 600": "مدة صلاحية التخزين المؤقت بالثواني، الافتراضي: 600", - "Calculate folder statistics": "حساب إحصاءات المجلد", - "Calculate statistics for {prefix}": "حساب إحصاءات {prefix}", - "Cancel statistics for {prefix}": "إلغاء إحصاءات {prefix}", - "Calculating folder statistics...": "جارٍ حساب إحصاءات المجلد...", - "Could not calculate folder statistics.": "تعذر حساب إحصاءات المجلد.", - "Calculation cancelled.": "تم إلغاء الحساب.", - "Folder exceeds the scan limit.": "يتجاوز المجلد حد الفحص.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "تفحص إحصاءات المجلد حتى 100 صفحة أو 100,000 كائن حالي، أيهما يتحقق أولاً. يعتمد آخر تعديل على هذه الكائنات؛ قد تتغير النتائج أثناء الفحص.", "Cancel": "إلغاء", "Cancel Deletion": "إلغاء الحذف", "Cancel Key Deletion": "إلغاء حذف المفتاح", diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index 707312ff..a653368e 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Cache-Löschung mit Warnungen abgeschlossen", "Cache cleared successfully": "Cache erfolgreich gelöscht", "Cache time-to-live in seconds, default: 600": "Cache-Lebensdauer in Sekunden, Standard: 600", - "Calculate folder statistics": "Ordnerstatistik berechnen", - "Calculate statistics for {prefix}": "Statistik für {prefix} berechnen", - "Cancel statistics for {prefix}": "Statistik für {prefix} abbrechen", - "Calculating folder statistics...": "Ordnerstatistik wird berechnet...", - "Could not calculate folder statistics.": "Ordnerstatistik konnte nicht berechnet werden.", - "Calculation cancelled.": "Berechnung abgebrochen.", - "Folder exceeds the scan limit.": "Ordner überschreitet das Scanlimit.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Die Ordnerstatistik durchsucht höchstens 100 Seiten oder 100.000 aktuelle Objekte, je nachdem, welches Limit zuerst erreicht wird. Die letzte Änderung basiert auf diesen Objekten; Ergebnisse können sich während des Scans ändern.", "Cancel": "Abbrechen", "Cancel Deletion": "Löschung abbrechen", "Cancel Key Deletion": "Schlüssellöschung abbrechen", diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index 61f29494..090e80fe 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -176,14 +176,6 @@ "Cache clear completed with warnings": "Cache clear completed with warnings", "Cache cleared successfully": "Cache cleared successfully", "Cache time-to-live in seconds, default: 600": "Cache time-to-live in seconds, default: 600", - "Calculate folder statistics": "Calculate folder statistics", - "Calculate statistics for {prefix}": "Calculate statistics for {prefix}", - "Cancel statistics for {prefix}": "Cancel statistics for {prefix}", - "Calculating folder statistics...": "Calculating folder statistics...", - "Could not calculate folder statistics.": "Could not calculate folder statistics.", - "Calculation cancelled.": "Calculation cancelled.", - "Folder exceeds the scan limit.": "Folder exceeds the scan limit.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.", "Cancel": "Cancel", "Cancel Deletion": "Cancel Deletion", "Cancel Key Deletion": "Cancel Key Deletion", diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index 321fb373..3fc25920 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Limpieza de caché completada con advertencias", "Cache cleared successfully": "Caché limpiada exitosamente", "Cache time-to-live in seconds, default: 600": "Tiempo de vida de la caché en segundos, predeterminado: 600", - "Calculate folder statistics": "Calcular estadísticas de carpeta", - "Calculate statistics for {prefix}": "Calcular estadísticas de {prefix}", - "Cancel statistics for {prefix}": "Cancelar estadísticas de {prefix}", - "Calculating folder statistics...": "Calculando estadísticas de carpeta...", - "Could not calculate folder statistics.": "No se pudieron calcular las estadísticas.", - "Calculation cancelled.": "Cálculo cancelado.", - "Folder exceeds the scan limit.": "La carpeta supera el límite de exploración.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Las estadísticas exploran hasta 100 páginas o 100.000 objetos actuales, lo que ocurra primero. La última modificación se basa en estos objetos; los resultados pueden cambiar durante la exploración.", "Cancel": "Cancelar", "Cancel Deletion": "Cancelar eliminación", "Cancel Key Deletion": "Cancelar eliminación de clave", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 90840554..4caff00b 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Effacement du cache terminé avec des avertissements", "Cache cleared successfully": "Cache effacé avec succès", "Cache time-to-live in seconds, default: 600": "Durée de vie du cache en secondes, par défaut : 600", - "Calculate folder statistics": "Calculer les statistiques du dossier", - "Calculate statistics for {prefix}": "Calculer les statistiques de {prefix}", - "Cancel statistics for {prefix}": "Annuler les statistiques de {prefix}", - "Calculating folder statistics...": "Calcul des statistiques du dossier...", - "Could not calculate folder statistics.": "Impossible de calculer les statistiques.", - "Calculation cancelled.": "Calcul annulé.", - "Folder exceeds the scan limit.": "Le dossier dépasse la limite d’analyse.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Les statistiques analysent au maximum 100 pages ou 100 000 objets actuels, selon la première limite atteinte. La dernière modification concerne ces objets ; les résultats peuvent changer pendant l’analyse.", "Cancel": "Annuler", "Cancel Deletion": "Annuler la suppression", "Cancel Key Deletion": "Annuler la suppression de la clé", diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index 1e22dca0..a09f8c0a 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Pembersihan cache selesai dengan peringatan", "Cache cleared successfully": "Cache berhasil dibersihkan", "Cache time-to-live in seconds, default: 600": "Time-to-live cache dalam detik, default: 600", - "Calculate folder statistics": "Hitung statistik folder", - "Calculate statistics for {prefix}": "Hitung statistik untuk {prefix}", - "Cancel statistics for {prefix}": "Batalkan statistik untuk {prefix}", - "Calculating folder statistics...": "Menghitung statistik folder...", - "Could not calculate folder statistics.": "Statistik folder tidak dapat dihitung.", - "Calculation cancelled.": "Penghitungan dibatalkan.", - "Folder exceeds the scan limit.": "Folder melampaui batas pemindaian.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Statistik folder memindai hingga 100 halaman atau 100.000 objek saat ini, mana pun yang tercapai lebih dahulu. Modifikasi terakhir berdasarkan objek tersebut; hasil dapat berubah selama pemindaian.", "Cancel": "Batal", "Cancel Deletion": "Batalkan Penghapusan", "Cancel Key Deletion": "Batalkan Penghapusan Kunci", diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index 4fe41a25..0f2e5c4f 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Pulizia cache completata con avvisi", "Cache cleared successfully": "Cache pulita con successo", "Cache time-to-live in seconds, default: 600": "Tempo di vita cache in secondi, predefinito: 600", - "Calculate folder statistics": "Calcola statistiche cartella", - "Calculate statistics for {prefix}": "Calcola statistiche per {prefix}", - "Cancel statistics for {prefix}": "Annulla statistiche per {prefix}", - "Calculating folder statistics...": "Calcolo statistiche cartella...", - "Could not calculate folder statistics.": "Impossibile calcolare le statistiche.", - "Calculation cancelled.": "Calcolo annullato.", - "Folder exceeds the scan limit.": "La cartella supera il limite di scansione.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Le statistiche analizzano fino a 100 pagine o 100.000 oggetti correnti, a seconda del limite raggiunto per primo. L’ultima modifica si basa su questi oggetti; i risultati possono cambiare durante la scansione.", "Cancel": "Annulla", "Cancel Deletion": "Annulla eliminazione", "Cancel Key Deletion": "Annulla eliminazione chiave", diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index 81d64f42..f8ee7b02 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "警告付きでキャッシュクリアが完了しました", "Cache cleared successfully": "キャッシュが正常にクリアされました", "Cache time-to-live in seconds, default: 600": "キャッシュの生存時間(秒)、デフォルト: 600", - "Calculate folder statistics": "フォルダーの統計を計算", - "Calculate statistics for {prefix}": "{prefix} の統計を計算", - "Cancel statistics for {prefix}": "{prefix} の統計をキャンセル", - "Calculating folder statistics...": "フォルダーの統計を計算中...", - "Could not calculate folder statistics.": "フォルダーの統計を計算できませんでした。", - "Calculation cancelled.": "計算をキャンセルしました。", - "Folder exceeds the scan limit.": "フォルダーがスキャン上限を超えています。", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "フォルダーの統計は最大 100 ページまたは現在のオブジェクト 100,000 件のいずれか先に達するまでスキャンします。最終更新はこれらに基づき、スキャン中に結果が変わる場合があります。", "Cancel": "キャンセル", "Cancel Deletion": "削除をキャンセル", "Cancel Key Deletion": "キー削除をキャンセル", diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index 67eafa77..2760cfcf 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "경고와 함께 캐시 지우기 완료", "Cache cleared successfully": "캐시가 성공적으로 지워짐", "Cache time-to-live in seconds, default: 600": "캐시 생존 시간(초), 기본값: 600", - "Calculate folder statistics": "폴더 통계 계산", - "Calculate statistics for {prefix}": "{prefix} 통계 계산", - "Cancel statistics for {prefix}": "{prefix} 통계 취소", - "Calculating folder statistics...": "폴더 통계 계산 중...", - "Could not calculate folder statistics.": "폴더 통계를 계산할 수 없습니다.", - "Calculation cancelled.": "계산이 취소되었습니다.", - "Folder exceeds the scan limit.": "폴더가 스캔 한도를 초과했습니다.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "폴더 통계는 최대 100페이지 또는 현재 객체 100,000개 중 먼저 도달하는 한도까지 스캔합니다. 최근 수정 시간은 이 객체를 기준으로 하며 스캔 중 결과가 바뀔 수 있습니다.", "Cancel": "취소", "Cancel Deletion": "삭제 취소", "Cancel Key Deletion": "키 삭제 취소", diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index 5d35237e..c49c08fd 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Limpeza de cache concluída com avisos", "Cache cleared successfully": "Cache limpo com sucesso", "Cache time-to-live in seconds, default: 600": "Tempo de vida do cache em segundos, padrão: 600", - "Calculate folder statistics": "Calcular estatísticas da pasta", - "Calculate statistics for {prefix}": "Calcular estatísticas de {prefix}", - "Cancel statistics for {prefix}": "Cancelar estatísticas de {prefix}", - "Calculating folder statistics...": "Calculando estatísticas da pasta...", - "Could not calculate folder statistics.": "Não foi possível calcular as estatísticas.", - "Calculation cancelled.": "Cálculo cancelado.", - "Folder exceeds the scan limit.": "A pasta excede o limite de varredura.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "As estatísticas verificam até 100 páginas ou 100.000 objetos atuais, o que ocorrer primeiro. A última modificação considera esses objetos; os resultados podem mudar durante a varredura.", "Cancel": "Cancelar", "Cancel Deletion": "Cancelar exclusão", "Cancel Key Deletion": "Cancelar exclusão da chave", diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index 8d0c2777..f35fe3a0 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Очистка кэша завершена с предупреждениями", "Cache cleared successfully": "Кэш успешно очищен", "Cache time-to-live in seconds, default: 600": "Время жизни кэша в секундах, по умолчанию: 600", - "Calculate folder statistics": "Рассчитать статистику папки", - "Calculate statistics for {prefix}": "Рассчитать статистику для {prefix}", - "Cancel statistics for {prefix}": "Отменить статистику для {prefix}", - "Calculating folder statistics...": "Расчёт статистики папки...", - "Could not calculate folder statistics.": "Не удалось рассчитать статистику папки.", - "Calculation cancelled.": "Расчёт отменён.", - "Folder exceeds the scan limit.": "Папка превышает лимит сканирования.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Статистика папки охватывает до 100 страниц или 100 000 текущих объектов, в зависимости от того, какой предел достигнут первым. Время изменения относится к ним; результаты могут измениться во время сканирования.", "Cancel": "Отмена", "Cancel Deletion": "Отменить удаление", "Cancel Key Deletion": "Отменить удаление ключа", diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index 0b09da5a..ef6fded4 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Önbellek temizleme işlemi uyarılarla tamamlandı", "Cache cleared successfully": "Önbellek başarıyla temizlendi", "Cache time-to-live in seconds, default: 600": "Saniye cinsinden önbellek yaşam süresi, varsayılan: 600", - "Calculate folder statistics": "Klasör istatistiklerini hesapla", - "Calculate statistics for {prefix}": "{prefix} için istatistikleri hesapla", - "Cancel statistics for {prefix}": "{prefix} istatistiklerini iptal et", - "Calculating folder statistics...": "Klasör istatistikleri hesaplanıyor...", - "Could not calculate folder statistics.": "Klasör istatistikleri hesaplanamadı.", - "Calculation cancelled.": "Hesaplama iptal edildi.", - "Folder exceeds the scan limit.": "Klasör tarama sınırını aşıyor.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Klasör istatistikleri en fazla 100 sayfa veya 100.000 geçerli nesneyi, hangisine önce ulaşılırsa, tarar. Son değişiklik bu nesnelere dayanır; sonuçlar tarama sırasında değişebilir.", "Cancel": "İptal", "Cancel Deletion": "Silmeyi iptal et", "Cancel Key Deletion": "Anahtar silmeyi iptal et", diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index d4f84c0f..a10eefd5 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -179,14 +179,6 @@ "Cache clear completed with warnings": "Hoàn tất xóa bộ nhớ đệm với một số cảnh báo", "Cache cleared successfully": "Đã xóa bộ nhớ đệm thành công", "Cache time-to-live in seconds, default: 600": "Thời gian tồn tại của bộ nhớ đệm tính bằng giây, mặc định: 600", - "Calculate folder statistics": "Tính thống kê thư mục", - "Calculate statistics for {prefix}": "Tính thống kê cho {prefix}", - "Cancel statistics for {prefix}": "Hủy thống kê cho {prefix}", - "Calculating folder statistics...": "Đang tính thống kê thư mục...", - "Could not calculate folder statistics.": "Không thể tính thống kê thư mục.", - "Calculation cancelled.": "Đã hủy tính toán.", - "Folder exceeds the scan limit.": "Thư mục vượt giới hạn quét.", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "Thống kê thư mục quét tối đa 100 trang hoặc 100.000 đối tượng hiện tại, tùy giới hạn nào đạt trước. Thời gian sửa đổi dựa trên các đối tượng này; kết quả có thể thay đổi trong khi quét.", "Cancel": "Hủy bỏ", "Cancel Deletion": "Hủy xóa", "Cancel Key Deletion": "Hủy xóa khóa", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 9513dfe8..c36b3bcb 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -176,14 +176,6 @@ "Cache clear completed with warnings": "缓存清除完成,但有警告", "Cache cleared successfully": "缓存清除成功", "Cache time-to-live in seconds, default: 600": "缓存生存时间(秒),默认:600", - "Calculate folder statistics": "计算文件夹统计", - "Calculate statistics for {prefix}": "计算 {prefix} 的统计", - "Cancel statistics for {prefix}": "取消 {prefix} 的统计", - "Calculating folder statistics...": "正在计算文件夹统计...", - "Could not calculate folder statistics.": "无法计算文件夹统计。", - "Calculation cancelled.": "计算已取消。", - "Folder exceeds the scan limit.": "文件夹超出扫描上限。", - "Folder statistics scan up to 100 pages or 100,000 current objects, whichever comes first. Latest modification is based on these objects; results may change during the scan.": "文件夹统计最多扫描 100 页或 100,000 个当前对象,以先达到的上限为准。最近修改时间基于这些对象;扫描期间结果可能发生变化。", "Cancel": "取消", "Cancel Deletion": "取消删除", "Cancel Key Deletion": "取消密钥删除", diff --git a/lib/prefix-summary-request.ts b/lib/prefix-summary-request.ts deleted file mode 100644 index e6f0c39b..00000000 --- a/lib/prefix-summary-request.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ListObjectsV2Command, type S3Client } from "@aws-sdk/client-s3" -import { summarizePrefix } from "./prefix-summary" - -export function fetchPrefixSummary(client: S3Client, bucket: string, prefix: string, signal: AbortSignal) { - return summarizePrefix( - (token, abortSignal) => - client.send( - new ListObjectsV2Command({ - Bucket: bucket, - Prefix: prefix, - MaxKeys: 1000, - ContinuationToken: token, - EncodingType: "url", - }), - { abortSignal }, - ), - signal, - ) -} diff --git a/lib/prefix-summary.ts b/lib/prefix-summary.ts deleted file mode 100644 index f0cf0576..00000000 --- a/lib/prefix-summary.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface PrefixSummaryPage { - Contents?: Array<{ Size?: number; LastModified?: Date }> - IsTruncated?: boolean - NextContinuationToken?: string -} - -export class PrefixSummaryLimitError extends Error {} - -// A bounded, explicit scan. S3 pagination is not a point-in-time snapshot. -export async function summarizePrefix( - fetchPage: (token: string | undefined, signal: AbortSignal) => Promise, - signal: AbortSignal, -) { - let count = 0 - let size = 0 - let lastModified: Date | undefined - let token: string | undefined - const seen = new Set() - for (let page = 0; page < 100; page++) { - signal.throwIfAborted() - const response = await fetchPage(token, signal) - signal.throwIfAborted() - for (const object of response.Contents ?? []) { - if (++count > 100_000) throw new PrefixSummaryLimitError("Prefix is too large to calculate in one scan") - const timestamp = object.LastModified?.getTime() - if ( - !Number.isSafeInteger(object.Size) || - object.Size! < 0 || - timestamp === undefined || - !Number.isFinite(timestamp) - ) { - throw new Error("Incomplete object metadata") - } - size += object.Size! - if (!Number.isSafeInteger(size)) throw new Error("Prefix size exceeds the supported range") - if (!lastModified || timestamp > lastModified.getTime()) lastModified = object.LastModified - } - if (response.IsTruncated === false) return { size, lastModified } - const next = response.NextContinuationToken - if (response.IsTruncated !== true || !next || seen.has(next)) throw new Error("Invalid pagination response") - seen.add(next) - token = next - } - throw new PrefixSummaryLimitError("Prefix is too large to calculate in one scan") -} diff --git a/tests/lib/force-cleanup-components.test.ts b/tests/lib/force-cleanup-components.test.ts index 0d323302..8a5868bd 100644 --- a/tests/lib/force-cleanup-components.test.ts +++ b/tests/lib/force-cleanup-components.test.ts @@ -6,7 +6,6 @@ import { createRoot } from "react-dom/client" import { Window, type Element as HappyElement } from "happy-dom" import ts from "typescript" import { isForceCleanupTarget } from "../../lib/permission-capabilities" -import { PrefixSummaryLimitError } from "../../lib/prefix-summary" import type { ObjectList as ObjectListComponent } from "../../components/object/list" import type { TaskItem as TaskItemComponent } from "../../components/tasks/item" import type { AnyTask } from "../../contexts/task-context" @@ -98,11 +97,10 @@ const mocks: Record = { }) => React.createElement("input", { type: "checkbox", checked, id, onChange: (e) => onCheckedChange(e.target.checked) }), PageHeader: ({ children, actions }: UiProps) => React.createElement("header", null, actions, children), - DataTable: ({ table, toolbar }: { table: MockTable; toolbar?: React.ReactNode }) => + DataTable: ({ table }: { table: MockTable }) => React.createElement( "section", null, - toolbar, table.data.map((row) => React.createElement( "div", @@ -119,22 +117,8 @@ const mocks: Record = { }), useLocalStorage: (_key: string, initial: boolean) => React.useState(initial), useObjectListPagination() {}, - SearchInput: ({ - value, - onChange, - placeholder, - }: { - value: string - onChange: (value: string) => void - placeholder: string - }) => - React.createElement("input", { - value, - placeholder, - onInput: (event: React.FormEvent) => onChange(event.currentTarget.value), - }), formatBytes: (bytes: number) => String(bytes), - formatDateTime: (date: string | Date) => (date instanceof Date ? date.toISOString() : date), + formatDateTime: (date: string) => date, } for (const name of [ "AlertDialogContent", @@ -198,12 +182,7 @@ const fullKey = "parent/中文%2F+space /child/" const consequences = "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone." -async function mount( - t: TestContext, - initialProps: ObjectListProps, - versioning: string = "Suspended", - calculatePrefixSummary?: (key: string, signal: AbortSignal) => Promise<{ size: number; lastModified?: Date }>, -) { +async function mount(t: TestContext, initialProps: ObjectListProps, versioning: string = "Suspended") { const browser = new Window() const globals = { window: browser, document: browser.document, IS_REACT_ACT_ENVIRONMENT: true } const descriptors = Object.keys(globals).map( @@ -226,7 +205,6 @@ async function mount( CommonPrefixes: folders.map((Prefix) => ({ Prefix })), Contents: [{ Key: "file.txt" }, { Key: "literal-object/" }], }), - calculatePrefixSummary, getSignedUrl: async () => "", renameObject: async () => {}, } @@ -273,7 +251,7 @@ async function mount( (element) => element.getAttribute("data-object-key") === key, ) assert.ok(row, `expected row ${key}`) - await click(row.querySelector('button[aria-label^="Actions:"]')) + await click(row.querySelector("button[aria-label]")) return row } t.after(async () => { @@ -444,140 +422,3 @@ test("successful tasks do not display stale backend errors", async (t) => { await React.act(async () => ui.root.render(React.createElement(TaskItem, { task }))) assert.equal(ui.container.querySelector('[role="alert"]'), null) }) - -test("folder statistics do not land in a new bucket after a scope change", async (t) => { - const response = Promise.withResolvers<{ size: number; lastModified?: Date }>() - let usedSignal: AbortSignal | undefined - const ui = await mount(t, props, "Suspended", async (_key, signal) => { - usedSignal = signal - return response.promise - }) - await ui.click(ui.findButton("Calculate folder statistics")) - assert.equal(usedSignal?.aborted, false) - await ui.render({ bucket: "other-bucket" }) - assert.equal(usedSignal?.aborted, true) - await React.act(async () => response.resolve({ size: 731, lastModified: new Date("2026-01-01") })) - assert.equal(ui.container.textContent?.includes("731"), false) -}) - -test("refresh cancels folder statistics and ignores a late result", async (t) => { - const response = Promise.withResolvers<{ size: number; lastModified?: Date }>() - let usedSignal: AbortSignal | undefined - const ui = await mount(t, props, "Suspended", async (_key, signal) => { - usedSignal = signal - return response.promise - }) - await ui.click(ui.findButton("Calculate folder statistics")) - await ui.render({ refreshTrigger: 1 }) - assert.equal(usedSignal?.aborted, true) - await React.act(async () => response.resolve({ size: 731 })) - assert.equal(ui.container.textContent?.includes("731"), false) -}) - -test("folder statistics update only the requested row and clear on refresh", async (t) => { - const ui = await mount(t, props, "Suspended", async () => ({ size: 731 })) - await ui.click(ui.findButton("Calculate folder statistics")) - const rows = Array.from(ui.container.querySelectorAll("[data-object-key]")) - assert.ok(rows.find((row) => row.getAttribute("data-object-key") === fullKey)?.textContent?.includes("731")) - assert.equal( - rows.find((row) => row.getAttribute("data-object-key") === "other/")?.textContent?.includes("731"), - false, - ) - await ui.render({ refreshTrigger: 1 }) - assert.equal(ui.container.textContent?.includes("731"), false) -}) - -test("folder scan remains cancellable when search hides its row, and a late response is ignored", async (t) => { - const response = Promise.withResolvers<{ size: number; lastModified?: Date }>() - let usedSignal: AbortSignal | undefined - let calls = 0 - const ui = await mount(t, props, "Suspended", async (_key, signal) => { - calls++ - usedSignal = signal - return response.promise - }) - await ui.click(ui.findButton("Calculate folder statistics")) - const search = ui.container.querySelector('input[placeholder="Filter loaded objects"]') as HTMLInputElement | null - assert.ok(search) - await React.act(async () => { - search.value = "other" - search.dispatchEvent(new search.ownerDocument.defaultView!.Event("input", { bubbles: true })) - }) - assert.equal(ui.container.querySelector(`[data-object-key="${fullKey}"]`), null) - assert.ok(ui.container.querySelector('[role="status"]')?.textContent?.includes(fullKey)) - const otherScan = ui.findButton("Calculate folder statistics") - assert.ok(otherScan) - assert.equal((otherScan as unknown as HTMLButtonElement).disabled, true) - await ui.click(otherScan) - assert.equal(calls, 1) - await ui.click(ui.findButton("Cancel")) - assert.equal(usedSignal?.aborted, true) - await React.act(async () => response.resolve({ size: 731, lastModified: new Date("2026-01-01") })) - assert.ok(ui.container.querySelector('[role="status"]')?.textContent?.includes("Calculation cancelled.")) - assert.ok(ui.container.querySelector('[role="status"]')?.textContent?.includes(fullKey)) - await ui.click(ui.findButton("Close")) - assert.equal(ui.container.querySelector('[role="status"]'), null) - await React.act(async () => { - search.value = "" - search.dispatchEvent(new search.ownerDocument.defaultView!.Event("input", { bubbles: true })) - }) - assert.equal(ui.container.textContent?.includes("731"), false) -}) - -for (const [error, expected] of [ - [new Error("network"), "Could not calculate folder statistics."], - [new PrefixSummaryLimitError("limit"), "Folder exceeds the scan limit."], -] as const) { - test(`a hidden folder scan reports ${expected} until closed`, async (t) => { - const response = Promise.withResolvers<{ size: number; lastModified?: Date }>() - const ui = await mount(t, props, "Suspended", async () => response.promise) - await ui.click(ui.findButton("Calculate folder statistics")) - const search = ui.container.querySelector('input[placeholder="Filter loaded objects"]') as HTMLInputElement | null - assert.ok(search) - await React.act(async () => { - search.value = "other" - search.dispatchEvent(new search.ownerDocument.defaultView!.Event("input", { bubbles: true })) - }) - assert.equal(ui.container.querySelector(`[data-object-key="${fullKey}"]`), null) - await React.act(async () => response.reject(error)) - assert.ok(ui.container.querySelector('[role="alert"]')?.textContent?.includes(expected)) - assert.ok(ui.container.querySelector('[role="alert"]')?.textContent?.includes(fullKey)) - await ui.click(ui.findButton("Close")) - assert.equal(ui.container.querySelector('[role="alert"]'), null) - }) -} - -for (const [summary, dateText] of [ - [{ size: 731, lastModified: new Date("2026-01-01") }, "2026-01-01T00:00:00.000Z"], - [{ size: 0 }, "-"], -] as const) { - test(`a hidden folder scan shows completed statistics (${dateText}) and can be closed`, async (t) => { - const response = Promise.withResolvers<{ size: number; lastModified?: Date }>() - const ui = await mount(t, props, "Suspended", async () => response.promise) - await ui.click(ui.findButton("Calculate folder statistics")) - const search = ui.container.querySelector('input[placeholder="Filter loaded objects"]') as HTMLInputElement | null - assert.ok(search) - await React.act(async () => { - search.value = "other" - search.dispatchEvent(new search.ownerDocument.defaultView!.Event("input", { bubbles: true })) - }) - assert.equal(ui.container.querySelector(`[data-object-key="${fullKey}"]`), null) - await React.act(async () => response.resolve(summary)) - const status = ui.container.querySelector('[role="status"]') - assert.ok(status?.textContent?.includes("Complete")) - assert.ok(status?.textContent?.includes(fullKey)) - assert.ok(status?.textContent?.includes(`Size: ${summary.size}`)) - assert.ok(status?.textContent?.includes(`Last Modified: ${dateText}`)) - await ui.click(ui.findButton("Close")) - assert.equal(ui.container.querySelector('[role="status"]'), null) - await React.act(async () => { - search.value = "" - search.dispatchEvent(new search.ownerDocument.defaultView!.Event("input", { bubbles: true })) - }) - assert.ok(ui.container.querySelector(`[data-object-key="${fullKey}"]`)?.textContent?.includes(String(summary.size))) - await ui.render({ refreshTrigger: 1 }) - assert.equal(ui.container.querySelector('[role="status"]'), null) - const row = ui.container.querySelector(`[data-object-key="${fullKey}"]`) - assert.equal(row?.textContent?.includes(String(summary.size)), false) - }) -} diff --git a/tests/lib/prefix-summary-request.test.ts b/tests/lib/prefix-summary-request.test.ts deleted file mode 100644 index 0cc2b527..00000000 --- a/tests/lib/prefix-summary-request.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import test from "node:test" -import assert from "node:assert/strict" -import { ListObjectsV2Command, type S3Client } from "@aws-sdk/client-s3" -import { fetchPrefixSummary } from "../../lib/prefix-summary-request" - -test("requests each page using the literal prefix, URL encoding and abort signal without a delimiter", async () => { - const controller = new AbortController() - const calls: Array<{ command: ListObjectsV2Command; signal: AbortSignal }> = [] - const client = { - send: async (command: ListObjectsV2Command, { abortSignal }: { abortSignal: AbortSignal }) => { - calls.push({ command, signal: abortSignal }) - return calls.length === 1 - ? { IsTruncated: true, NextContinuationToken: "opaque" } - : { IsTruncated: false, Contents: [{ Size: 5, LastModified: new Date("2026-01-01") }] } - }, - } as unknown as S3Client - const summary = await fetchPrefixSummary(client, "bucket", "a//%☃/", controller.signal) - assert.equal(summary.size, 5) - assert.equal(calls.length, 2) - for (const { command, signal } of calls) { - assert.ok(command instanceof ListObjectsV2Command) - assert.equal(command.input.Prefix, "a//%☃/") - assert.equal(command.input.Delimiter, undefined) - assert.equal(command.input.EncodingType, "url") - assert.equal(signal, controller.signal) - } - assert.equal(calls[1].command.input.ContinuationToken, "opaque") -}) diff --git a/tests/lib/prefix-summary.test.ts b/tests/lib/prefix-summary.test.ts deleted file mode 100644 index 973d66af..00000000 --- a/tests/lib/prefix-summary.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import test from "node:test" -import assert from "node:assert/strict" -import { PrefixSummaryLimitError, summarizePrefix } from "../../lib/prefix-summary" - -const signal = () => new AbortController().signal -const object = (size: number, time: string) => ({ Size: size, LastModified: new Date(time) }) - -test("combines pages and returns the newest object time", async () => { - const result = await summarizePrefix( - async (token) => - token - ? { Contents: [object(4, "2026-02-01")], IsTruncated: false } - : { Contents: [object(3, "2026-01-01")], IsTruncated: true, NextContinuationToken: "next" }, - signal(), - ) - assert.deepEqual(result, { size: 7, lastModified: new Date("2026-02-01") }) -}) - -test("empty prefixes have zero size and no object time", async () => { - assert.deepEqual(await summarizePrefix(async () => ({ IsTruncated: false }), signal()), { - size: 0, - lastModified: undefined, - }) -}) - -test("rejects missing metadata and unsafe totals", async () => { - await assert.rejects(summarizePrefix(async () => ({ Contents: [{ Size: 2 }], IsTruncated: false }), signal())) - await assert.rejects( - summarizePrefix( - async () => ({ - Contents: [object(Number.MAX_SAFE_INTEGER, "2026-01-01"), object(1, "2026-01-01")], - IsTruncated: false, - }), - signal(), - ), - ) -}) - -test("rejects missing and repeated continuation tokens", async () => { - await assert.rejects(summarizePrefix(async () => ({ IsTruncated: true }), signal())) - await assert.rejects(summarizePrefix(async () => ({ IsTruncated: true, NextContinuationToken: "same" }), signal())) -}) - -test("limits long scans and propagates network errors and cancellation", async () => { - let pages = 0 - await assert.rejects( - summarizePrefix(async () => ({ IsTruncated: true, NextContinuationToken: String(++pages) }), signal()), - ) - assert.equal(pages, 100) - await assert.rejects( - summarizePrefix(async () => { - throw new Error("network") - }, signal()), - /network/, - ) - const controller = new AbortController() - controller.abort() - await assert.rejects(summarizePrefix(async () => ({ IsTruncated: false }), controller.signal)) -}) - -test("rejects malformed timestamps, oversized pages and missing truncation state", async () => { - await assert.rejects( - summarizePrefix(async () => ({ Contents: [object(1, "invalid")], IsTruncated: false }), signal()), - ) - await assert.rejects(summarizePrefix(async () => ({ IsTruncated: undefined }), signal())) - await assert.rejects( - summarizePrefix( - async () => ({ Contents: Array(100_001).fill(object(0, "2026-01-01")), IsTruncated: false }), - signal(), - ), - ) -}) - -test("cancellation after a page request never reports a partial result", async () => { - const controller = new AbortController() - await assert.rejects( - summarizePrefix(async () => { - controller.abort() - return { Contents: [object(1, "2026-01-01")], IsTruncated: false } - }, controller.signal), - ) -}) - -test("accepts a short final page at the 100-page limit", async () => { - let pages = 0 - const result = await summarizePrefix(async () => { - pages++ - return { - Contents: [object(1, "2026-01-01")], - IsTruncated: pages < 100, - NextContinuationToken: pages < 100 ? String(pages) : undefined, - } - }, signal()) - assert.equal(pages, 100) - assert.equal(result.size, 100) -}) - -test("rejects a truncated 100th short page without presenting partial statistics", async () => { - let pages = 0 - await assert.rejects( - summarizePrefix(async () => { - pages++ - return { - Contents: [object(1, "2026-01-01")], - IsTruncated: true, - NextContinuationToken: String(pages), - } - }, signal()), - PrefixSummaryLimitError, - ) - assert.equal(pages, 100) -})