mirror of
https://github.com/rustfs/console.git
synced 2026-10-30 19:33:40 +08:00
feat: use server zip downloads (#128)
This commit is contained in:
+22
-69
@@ -35,6 +35,7 @@ import { useObject } from "@/hooks/use-object"
|
||||
import { useBucket } from "@/hooks/use-bucket"
|
||||
import { useLocalStorage } from "@/hooks/use-local-storage"
|
||||
import { usePermissions } from "@/hooks/use-permissions"
|
||||
import { useApi } from "@/contexts/api-context"
|
||||
import { useMessage } from "@/lib/feedback/message"
|
||||
import { exportFile } from "@/lib/export-file"
|
||||
import { getContentType } from "@/lib/mime-types"
|
||||
@@ -56,12 +57,15 @@ import {
|
||||
shouldShowDeleteAllVersions,
|
||||
type BucketVersioningState,
|
||||
} from "@/lib/object-delete"
|
||||
import {
|
||||
buildObjectZipDownloadPayload,
|
||||
normalizeObjectZipDownloadUrl,
|
||||
type CreateObjectZipDownloadResponse,
|
||||
} from "@/lib/object-zip-download"
|
||||
import { TaskStatsButton } from "@/components/tasks/stats-button"
|
||||
import { useAddDeleteKeys, useAddDeleteFolder, useTasks } from "@/contexts/task-context"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import dayjs from "dayjs"
|
||||
import { saveAs } from "file-saver"
|
||||
import JSZip from "jszip"
|
||||
|
||||
interface ObjectRow {
|
||||
Key: string
|
||||
@@ -95,7 +99,8 @@ export function ObjectList({
|
||||
}: ObjectListProps) {
|
||||
const { t } = useTranslation()
|
||||
const message = useMessage()
|
||||
const { listObject, getSignedUrl, mapAllFiles } = useObject(bucket)
|
||||
const api = useApi()
|
||||
const { listObject, getSignedUrl } = useObject(bucket)
|
||||
const { getBucketVersioning } = useBucket()
|
||||
const { canCapability } = usePermissions()
|
||||
const addDeleteKeys = useAddDeleteKeys()
|
||||
@@ -416,12 +421,6 @@ export function ObjectList({
|
||||
|
||||
const checkedKeys = selectedRowIds
|
||||
|
||||
const computeRelativeKey = (key: string) => {
|
||||
if (!key) return ""
|
||||
const base = prefix || ""
|
||||
return base && key.startsWith(base) ? key.slice(base.length) : key
|
||||
}
|
||||
|
||||
const downloadMultiple = async () => {
|
||||
if (!checkedKeys.length) {
|
||||
message.warning(t("Please select at least one item"))
|
||||
@@ -434,74 +433,28 @@ export function ObjectList({
|
||||
return
|
||||
}
|
||||
|
||||
const collecting = message.loading(t("Collecting files"), { duration: 0 })
|
||||
const allFiles: { key: string; relative: string }[] = []
|
||||
const loadingMsg = message.loading(t("Preparing download"), { duration: 0 })
|
||||
|
||||
try {
|
||||
for (const item of selectedRows) {
|
||||
if (item.type === "object") {
|
||||
allFiles.push({
|
||||
key: item.Key,
|
||||
relative: computeRelativeKey(item.Key),
|
||||
})
|
||||
} else if (item.type === "prefix") {
|
||||
await mapAllFiles(bucket, item.Key, (fileKey) => {
|
||||
allFiles.push({
|
||||
key: fileKey,
|
||||
relative: computeRelativeKey(fileKey),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
collecting.destroy()
|
||||
message.error((err as Error)?.message ?? t("Download Failed"))
|
||||
return
|
||||
}
|
||||
|
||||
collecting.destroy()
|
||||
|
||||
if (!allFiles.length) {
|
||||
message.warning(t("No files to download"))
|
||||
return
|
||||
}
|
||||
|
||||
const zip = new JSZip()
|
||||
let finished = 0
|
||||
const destroyRef: { current: (() => void) | null } = { current: null }
|
||||
|
||||
const updateProgress = () => {
|
||||
if (destroyRef.current) destroyRef.current()
|
||||
const handle = message.loading(`${t("Downloading files")} ${Math.round((finished / allFiles.length) * 100)}%`, {
|
||||
duration: 0,
|
||||
const payload = buildObjectZipDownloadPayload({
|
||||
bucket,
|
||||
basePrefix: prefix,
|
||||
rows: selectedRows,
|
||||
})
|
||||
destroyRef.current = handle.destroy
|
||||
}
|
||||
const response = (await api.post("/object-zip-downloads", payload)) as CreateObjectZipDownloadResponse
|
||||
|
||||
updateProgress()
|
||||
if (!response.download_url) {
|
||||
throw new Error(t("Download Failed"))
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
allFiles.map(async ({ key, relative }) => {
|
||||
const url = await getSignedUrl(key)
|
||||
const response = await fetch(url)
|
||||
const blob = await response.blob()
|
||||
zip.file(relative || key, blob)
|
||||
finished++
|
||||
updateProgress()
|
||||
}),
|
||||
)
|
||||
window.location.href = normalizeObjectZipDownloadUrl(response.download_url, api.resolveUrl("/"))
|
||||
table.resetRowSelection()
|
||||
message.success(t("Download ready"))
|
||||
} catch (err) {
|
||||
if (destroyRef.current) destroyRef.current()
|
||||
message.error((err as Error)?.message ?? t("Download Failed"))
|
||||
return
|
||||
} finally {
|
||||
loadingMsg.destroy()
|
||||
}
|
||||
|
||||
if (destroyRef.current) destroyRef.current()
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" })
|
||||
saveAs(content, `${bucket ?? "download"}.zip`)
|
||||
message.success(t("Download ready"))
|
||||
}
|
||||
|
||||
const openDeleteDialog = (keys: string[]) => {
|
||||
|
||||
@@ -40,6 +40,10 @@ export class ApiClient {
|
||||
this.errorHandler = handler
|
||||
}
|
||||
|
||||
resolveUrl(url: string): string {
|
||||
return new URL(url, this.config?.baseUrl).toString()
|
||||
}
|
||||
|
||||
async request(url: string, options: RequestOptions = {}, parseJson: boolean = true) {
|
||||
url = this.config?.baseUrl ? joinURL(this.config?.baseUrl, url) : url
|
||||
options.headers = { ...this.config?.headers, ...options.headers }
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
function sanitizeZipFilename(value) {
|
||||
const cleaned = value
|
||||
.trim()
|
||||
.replace(/[\\/:*?"<>|]+/g, "-")
|
||||
.replace(/^\.+$/, "")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
|
||||
return cleaned || "download"
|
||||
}
|
||||
|
||||
export function getObjectZipDownloadFilename(bucket, basePrefix) {
|
||||
const normalizedPrefix = basePrefix.replace(/\/+$/g, "")
|
||||
const name = normalizedPrefix ? normalizedPrefix.split("/").pop() : bucket
|
||||
|
||||
return `${sanitizeZipFilename(name || bucket || "download")}.zip`
|
||||
}
|
||||
|
||||
export function buildObjectZipDownloadPayload({ bucket, basePrefix, rows }) {
|
||||
return {
|
||||
bucket,
|
||||
prefix: basePrefix,
|
||||
objects: rows.filter((row) => row.type === "object").map((row) => row.Key),
|
||||
prefixes: rows.filter((row) => row.type === "prefix").map((row) => row.Key),
|
||||
filename: getObjectZipDownloadFilename(bucket, basePrefix),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeObjectZipDownloadUrl(downloadUrl, origin) {
|
||||
return new URL(downloadUrl, origin).toString()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
export interface ObjectZipDownloadRow {
|
||||
Key: string
|
||||
type: "prefix" | "object"
|
||||
}
|
||||
|
||||
export interface BuildObjectZipDownloadPayloadInput {
|
||||
bucket: string
|
||||
basePrefix: string
|
||||
rows: ObjectZipDownloadRow[]
|
||||
}
|
||||
|
||||
export interface ObjectZipDownloadPayload {
|
||||
bucket: string
|
||||
prefix: string
|
||||
objects: string[]
|
||||
prefixes: string[]
|
||||
filename: string
|
||||
}
|
||||
|
||||
export interface CreateObjectZipDownloadResponse {
|
||||
download_url: string
|
||||
expires_at?: string
|
||||
}
|
||||
|
||||
function sanitizeZipFilename(value: string) {
|
||||
const cleaned = value
|
||||
.trim()
|
||||
.replace(/[\\/:*?"<>|]+/g, "-")
|
||||
.replace(/^\.+$/, "")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
|
||||
return cleaned || "download"
|
||||
}
|
||||
|
||||
export function getObjectZipDownloadFilename(bucket: string, basePrefix: string) {
|
||||
const normalizedPrefix = basePrefix.replace(/\/+$/g, "")
|
||||
const name = normalizedPrefix ? normalizedPrefix.split("/").pop() : bucket
|
||||
|
||||
return `${sanitizeZipFilename(name || bucket || "download")}.zip`
|
||||
}
|
||||
|
||||
export function buildObjectZipDownloadPayload({
|
||||
bucket,
|
||||
basePrefix,
|
||||
rows,
|
||||
}: BuildObjectZipDownloadPayloadInput): ObjectZipDownloadPayload {
|
||||
return {
|
||||
bucket,
|
||||
prefix: basePrefix,
|
||||
objects: rows.filter((row) => row.type === "object").map((row) => row.Key),
|
||||
prefixes: rows.filter((row) => row.type === "prefix").map((row) => row.Key),
|
||||
filename: getObjectZipDownloadFilename(bucket, basePrefix),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeObjectZipDownloadUrl(downloadUrl: string, origin: string) {
|
||||
return new URL(downloadUrl, origin).toString()
|
||||
}
|
||||
@@ -30,13 +30,11 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"hyparquet": "^1.26.0",
|
||||
"hyparquet-compressors": "^1.1.1",
|
||||
"i18next": "^25.10.4",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"jszip": "^3.10.1",
|
||||
"next": "^16.2.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-forge": "^1.3.3",
|
||||
@@ -55,7 +53,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/node": "^20.19.37",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/react": "^19.2.14",
|
||||
|
||||
Generated
-91
@@ -249,9 +249,6 @@ importers:
|
||||
embla-carousel-react:
|
||||
specifier: ^8.6.0
|
||||
version: 8.6.0(react@19.2.4)
|
||||
file-saver:
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
hyparquet:
|
||||
specifier: ^1.26.0
|
||||
version: 1.26.0
|
||||
@@ -267,9 +264,6 @@ importers:
|
||||
input-otp:
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
next:
|
||||
specifier: ^16.2.1
|
||||
version: 16.2.1(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -319,9 +313,6 @@ importers:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/file-saver':
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7
|
||||
'@types/node':
|
||||
specifier: ^20.19.37
|
||||
version: 20.19.37
|
||||
@@ -1677,9 +1668,6 @@ packages:
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/file-saver@2.0.7':
|
||||
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
@@ -2129,9 +2117,6 @@ packages:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
cors@2.8.6:
|
||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -2615,9 +2600,6 @@ packages:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
file-saver@2.0.5:
|
||||
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2842,9 +2824,6 @@ packages:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
immer@10.2.0:
|
||||
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
|
||||
|
||||
@@ -3050,9 +3029,6 @@ packages:
|
||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
isarray@2.0.5:
|
||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||
|
||||
@@ -3120,9 +3096,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -3145,9 +3118,6 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -3462,9 +3432,6 @@ packages:
|
||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3557,9 +3524,6 @@ packages:
|
||||
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
prompts@2.4.2:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3675,9 +3639,6 @@ packages:
|
||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
recast@0.23.11:
|
||||
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -3759,9 +3720,6 @@ packages:
|
||||
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3805,9 +3763,6 @@ packages:
|
||||
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
@@ -3916,9 +3871,6 @@ packages:
|
||||
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
stringify-object@5.0.0:
|
||||
resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -5966,8 +5918,6 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/file-saver@2.0.7': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
@@ -6415,8 +6365,6 @@ snapshots:
|
||||
|
||||
cookie@1.1.1: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cors@2.8.6:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
@@ -7054,8 +7002,6 @@ snapshots:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
||||
file-saver@2.0.5: {}
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
@@ -7270,8 +7216,6 @@ snapshots:
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immer@10.2.0: {}
|
||||
|
||||
immer@11.1.8: {}
|
||||
@@ -7450,8 +7394,6 @@ snapshots:
|
||||
dependencies:
|
||||
is-inside-container: 1.0.0
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isarray@2.0.5: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
@@ -7510,13 +7452,6 @@ snapshots:
|
||||
object.assign: 4.1.7
|
||||
object.values: 1.2.1
|
||||
|
||||
jszip@3.10.1:
|
||||
dependencies:
|
||||
lie: 3.3.0
|
||||
pako: 1.0.11
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -7536,10 +7471,6 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
@@ -7846,8 +7777,6 @@ snapshots:
|
||||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -7916,8 +7845,6 @@ snapshots:
|
||||
dependencies:
|
||||
parse-ms: 4.0.0
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
prompts@2.4.2:
|
||||
dependencies:
|
||||
kleur: 3.0.3
|
||||
@@ -8022,16 +7949,6 @@ snapshots:
|
||||
|
||||
react@19.2.4: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
recast@0.23.11:
|
||||
dependencies:
|
||||
ast-types: 0.16.1
|
||||
@@ -8141,8 +8058,6 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
isarray: 2.0.5
|
||||
|
||||
safe-buffer@5.1.2: {}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -8209,8 +8124,6 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
shadcn@4.7.0(@types/node@20.19.37)(typescript@5.9.3):
|
||||
@@ -8412,10 +8325,6 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
stringify-object@5.0.0:
|
||||
dependencies:
|
||||
get-own-enumerable-keys: 1.0.0
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import fs from "node:fs"
|
||||
|
||||
test("ApiClient exposes URL resolution against its configured base URL", () => {
|
||||
const source = fs.readFileSync("lib/api-client.ts", "utf8")
|
||||
|
||||
assert.match(source, /resolveUrl\(url: string\): string/)
|
||||
assert.match(source, /new URL\(url, this\.config\?\.baseUrl\)\.toString\(\)/)
|
||||
})
|
||||
|
||||
test("object bulk download resolves relative download URLs through the API client", () => {
|
||||
const source = fs.readFileSync("components/object/list.tsx", "utf8")
|
||||
|
||||
assert.match(source, /normalizeObjectZipDownloadUrl\(response\.download_url, api\.resolveUrl\("\/"\)\)/)
|
||||
assert.doesNotMatch(source, /normalizeObjectZipDownloadUrl\(response\.download_url, window\.location\.origin\)/)
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import {
|
||||
buildObjectZipDownloadPayload,
|
||||
getObjectZipDownloadFilename,
|
||||
normalizeObjectZipDownloadUrl,
|
||||
} from "../../lib/object-zip-download.js"
|
||||
|
||||
test("buildObjectZipDownloadPayload separates object keys and prefixes", () => {
|
||||
const payload = buildObjectZipDownloadPayload({
|
||||
bucket: "photos",
|
||||
basePrefix: "2026/events/",
|
||||
rows: [
|
||||
{ Key: "2026/events/a.txt", type: "object" },
|
||||
{ Key: "2026/events/raw/", type: "prefix" },
|
||||
],
|
||||
})
|
||||
|
||||
assert.deepEqual(payload, {
|
||||
bucket: "photos",
|
||||
prefix: "2026/events/",
|
||||
objects: ["2026/events/a.txt"],
|
||||
prefixes: ["2026/events/raw/"],
|
||||
filename: "events.zip",
|
||||
})
|
||||
})
|
||||
|
||||
test("getObjectZipDownloadFilename falls back to bucket name at root", () => {
|
||||
assert.equal(getObjectZipDownloadFilename("photos", ""), "photos.zip")
|
||||
})
|
||||
|
||||
test("getObjectZipDownloadFilename uses the last prefix segment", () => {
|
||||
assert.equal(getObjectZipDownloadFilename("photos", "2026/events/"), "events.zip")
|
||||
})
|
||||
|
||||
test("getObjectZipDownloadFilename sanitizes unsafe filename characters", () => {
|
||||
assert.equal(getObjectZipDownloadFilename("photos", '2026/bad:name*?"<>|/'), "bad-name.zip")
|
||||
})
|
||||
|
||||
test("getObjectZipDownloadFilename falls back when sanitized names are empty", () => {
|
||||
assert.equal(getObjectZipDownloadFilename("...", ""), "download.zip")
|
||||
})
|
||||
|
||||
test("normalizeObjectZipDownloadUrl keeps absolute URLs", () => {
|
||||
assert.equal(
|
||||
normalizeObjectZipDownloadUrl("https://example.com/download.zip", "https://console.example.com"),
|
||||
"https://example.com/download.zip",
|
||||
)
|
||||
})
|
||||
|
||||
test("normalizeObjectZipDownloadUrl resolves relative URLs against the API base URL origin", () => {
|
||||
assert.equal(
|
||||
normalizeObjectZipDownloadUrl(
|
||||
"/rustfs/admin/v3/object-zip-downloads/abc.zip?token=abc",
|
||||
"https://api.example.com/rustfs/admin/v3",
|
||||
),
|
||||
"https://api.example.com/rustfs/admin/v3/object-zip-downloads/abc.zip?token=abc",
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user