Skip to content

Commit 35ba64e

Browse files
committed
feat: download multiple object selection as zip and each prefix as zip ignoring any deleted objects selected
1 parent 8cc6024 commit 35ba64e

File tree

13 files changed

+862
-28
lines changed

13 files changed

+862
-28
lines changed

portal-ui/src/api/consoleApi.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,6 +2139,28 @@ export class Api<
21392139
...params,
21402140
}),
21412141

2142+
/**
2143+
* No description
2144+
*
2145+
* @tags Object
2146+
* @name DownloadMultipleObjects
2147+
* @summary Download Multiple Objects
2148+
* @request POST:/buckets/{bucket_name}/objects/download-multiple
2149+
* @secure
2150+
*/
2151+
downloadMultipleObjects: (
2152+
bucketName: string,
2153+
objectList: string[],
2154+
params: RequestParams = {},
2155+
) =>
2156+
this.request<File, Error>({
2157+
path: `/buckets/${bucketName}/objects/download-multiple`,
2158+
method: "POST",
2159+
body: objectList,
2160+
secure: true,
2161+
...params,
2162+
}),
2163+
21422164
/**
21432165
* No description
21442166
*

portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,11 @@ const ListObjects = () => {
912912
createdTime = DateTime.fromISO(bucketInfo.creation_date);
913913
}
914914

915+
const downloadToolTip =
916+
selectedObjects?.length <= 1
917+
? "Download Selected"
918+
: ` Download selected objects as Zip. Note: Any prefix selected would download as a different zip. Any Deleted objects in the selection would be skipped from download.`;
919+
915920
const multiActionButtons = [
916921
{
917922
action: () => {
@@ -921,7 +926,7 @@ const ListObjects = () => {
921926
disabled: !canDownload || selectedObjects?.length === 0,
922927
icon: <DownloadIcon />,
923928
tooltip: canDownload
924-
? "Download Selected"
929+
? downloadToolTip
925930
: permissionTooltipHelper(
926931
[IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS],
927932
"download objects from this bucket",

portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,52 @@ import { BucketObjectItem } from "./ListObjects/types";
1818
import { encodeURLString } from "../../../../../common/utils";
1919
import { removeTrace } from "../../../ObjectBrowser/transferManager";
2020
import store from "../../../../../store";
21-
import { PermissionResource } from "api/consoleApi";
21+
import { ContentType, PermissionResource } from "api/consoleApi";
22+
import { api } from "../../../../../api";
23+
import { setErrorSnackMessage } from "../../../../../systemSlice";
24+
25+
const downloadWithLink = (href: string, downloadFileName: string) => {
26+
const link = document.createElement("a");
27+
link.href = href;
28+
link.download = downloadFileName;
29+
document.body.appendChild(link);
30+
link.click();
31+
document.body.removeChild(link);
32+
};
2233

34+
export const downloadSelectedAsZip = async (
35+
bucketName: string,
36+
objectList: string[],
37+
resultFileName: string,
38+
) => {
39+
const state = store.getState();
40+
const anonymousMode = state.system.anonymousMode;
41+
42+
try {
43+
const resp = await api.buckets.downloadMultipleObjects(
44+
bucketName,
45+
objectList,
46+
{
47+
type: ContentType.Json,
48+
headers: anonymousMode
49+
? {
50+
"X-Anonymous": "1",
51+
}
52+
: undefined,
53+
},
54+
);
55+
const blob = await resp.blob();
56+
const href = window.URL.createObjectURL(blob);
57+
downloadWithLink(href, resultFileName);
58+
} catch (err: any) {
59+
store.dispatch(
60+
setErrorSnackMessage({
61+
errorMessage: `Download of multiple files failed. ${err.statusText}`,
62+
detailedError: "",
63+
}),
64+
);
65+
}
66+
};
2367
export const download = (
2468
bucketName: string,
2569
objectPath: string,
@@ -33,8 +77,6 @@ export const download = (
3377
abortCallback: () => void,
3478
toastCallback: () => void,
3579
) => {
36-
const anchor = document.createElement("a");
37-
document.body.appendChild(anchor);
3880
let basename = document.baseURI.replace(window.location.origin, "");
3981
const state = store.getState();
4082
const anonymousMode = state.system.anonymousMode;
@@ -90,12 +132,7 @@ export const download = (
90132

91133
removeTrace(id);
92134

93-
var link = document.createElement("a");
94-
link.href = window.URL.createObjectURL(req.response);
95-
link.download = filename;
96-
document.body.appendChild(link);
97-
link.click();
98-
document.body.removeChild(link);
135+
downloadWithLink(window.URL.createObjectURL(req.response), filename);
99136
} else {
100137
if (req.getResponseHeader("Content-Type") === "application/json") {
101138
const rspBody: { detailedMessage?: string } = JSON.parse(

portal-ui/src/screens/Console/ObjectBrowser/objectBrowserThunks.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import { AppState } from "../../../store";
1919
import { encodeURLString, getClientOS } from "../../../common/utils";
2020
import { BucketObjectItem } from "../Buckets/ListBuckets/Objects/ListObjects/types";
2121
import { makeid, storeCallForObjectWithID } from "./transferManager";
22-
import { download } from "../Buckets/ListBuckets/Objects/utils";
22+
import {
23+
download,
24+
downloadSelectedAsZip,
25+
} from "../Buckets/ListBuckets/Objects/utils";
2326
import {
2427
cancelObjectInList,
2528
completeObject,
@@ -33,6 +36,7 @@ import {
3336
updateProgress,
3437
} from "./objectBrowserSlice";
3538
import { setSnackBarMessage } from "../../../systemSlice";
39+
import { DateTime } from "luxon";
3640

3741
export const downloadSelected = createAsyncThunk(
3842
"objectBrowser/downloadSelected",
@@ -104,21 +108,57 @@ export const downloadSelected = createAsyncThunk(
104108

105109
itemsToDownload = state.objectBrowser.records.filter(filterFunction);
106110

107-
// I case just one element is selected, then we trigger download modal validation.
108-
// We are going to enforce zip download when multiple files are selected
111+
// In case just one element is selected, then we trigger download modal validation.
109112
if (itemsToDownload.length === 1) {
110113
if (
111114
itemsToDownload[0].name.length > 200 &&
112115
getClientOS().toLowerCase().includes("win")
113116
) {
114117
dispatch(setDownloadRenameModal(itemsToDownload[0]));
115118
return;
119+
} else {
120+
downloadObject(itemsToDownload[0]);
121+
}
122+
} else {
123+
if (itemsToDownload.length === 1) {
124+
downloadObject(itemsToDownload[0]);
125+
} else if (itemsToDownload.length > 1) {
126+
const fileName = `${DateTime.now().toFormat(
127+
"LL-dd-yyyy-HH-mm-ss",
128+
)}_files_list.zip`;
129+
130+
const prefixesToDownload: BucketObjectItem[] = [];
131+
// We are enforcing zip download when multiple files are selected for better user experience
132+
const multiObjList = itemsToDownload.reduce((dwList: any[], bi) => {
133+
// Download only objects as zip, and download each prefix individually as zip.
134+
// Skip any deleted files selected via "Show deleted objects" in selection and log for debugging
135+
const isPrefix = bi?.name.endsWith("/");
136+
const isDeleted = bi?.delete_flag;
137+
138+
if (bi && !isPrefix && !isDeleted) {
139+
dwList.push(bi.name);
140+
} else {
141+
if (isPrefix && !isDeleted) {
142+
prefixesToDownload.push(bi);
143+
} else {
144+
console.log(`Skipping ${bi?.name} from download.`);
145+
}
146+
}
147+
return dwList;
148+
}, []);
149+
150+
// Download selected objects as Zip.
151+
// can we batch with some limit like 100 or 1000 files at a time?
152+
await downloadSelectedAsZip(bucketName, multiObjList, fileName);
153+
// now begin download of each selected prefix as zip
154+
if (prefixesToDownload.length) {
155+
prefixesToDownload.forEach((prefix) => {
156+
downloadObject(prefix);
157+
});
158+
}
159+
return;
116160
}
117161
}
118-
119-
itemsToDownload.forEach((filteredItem) => {
120-
downloadObject(filteredItem);
121-
});
122162
}
123163
},
124164
);

restapi/embedded_spec.go

Lines changed: 106 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

restapi/operations/console_api.go

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)