Skip to content

Commit 0dc7846

Browse files
authored
✨ [Frontend] Feature: multi download (#7495)
1 parent 0fb8613 commit 0dc7846

File tree

7 files changed

+231
-90
lines changed

7 files changed

+231
-90
lines changed

services/static-webserver/client/source/class/osparc/data/Resources.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,10 @@ qx.Class.define("osparc.data.Resources", {
12091209
method: "GET",
12101210
url: statics.API + "/storage/locations/{locationId}/paths?file_filter={path}&cursor={cursor}&size=1000"
12111211
},
1212+
multiDownload: {
1213+
method: "POST",
1214+
url: statics.API + "/storage/locations/{locationId}/export-data"
1215+
},
12121216
batchDelete: {
12131217
method: "POST",
12141218
url: statics.API + "/storage/locations/{locationId}/-/paths:batchDelete"

services/static-webserver/client/source/class/osparc/file/FileLabelWithActions.js

Lines changed: 143 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ qx.Class.define("osparc.file.FileLabelWithActions", {
110110
if (selectedItem) {
111111
this.__selection = [selectedItem];
112112
const isFile = osparc.file.FilesTree.isFile(selectedItem);
113-
this.getChildControl("download-button").setEnabled(isFile);
113+
const isMultiDownloadEnabled = osparc.utils.DisabledPlugins.isMultiDownloadEnabled();
114+
this.getChildControl("download-button").setEnabled(isFile || isMultiDownloadEnabled); // folders can also be downloaded
114115
this.getChildControl("delete-button").setEnabled(true); // folders can also be deleted
115116
this.getChildControl("selected-label").setValue(selectedItem.getLabel());
116117
} else {
@@ -142,16 +143,23 @@ qx.Class.define("osparc.file.FileLabelWithActions", {
142143
},
143144

144145
__retrieveURLAndDownloadSelected: function() {
146+
const isMultiDownloadEnabled = osparc.utils.DisabledPlugins.isMultiDownloadEnabled();
145147
if (this.isMultiSelect()) {
146-
this.__selection.forEach(selection => {
147-
if (selection && osparc.file.FilesTree.isFile(selection)) {
148-
this.__retrieveURLAndDownloadFile(selection);
149-
}
150-
});
148+
if (this.__selection.length === 1 && osparc.file.FilesTree.isFile(this.__selection[0])) {
149+
this.__retrieveURLAndDownloadFile(this.__selection[0]);
150+
} else if (this.__selection.length > 1 && isMultiDownloadEnabled) {
151+
const paths = this.__selection.map(item => item.getPath());
152+
this.__retrieveURLAndExportData(paths);
153+
}
151154
} else if (this.__selection.length) {
152155
const selection = this.__selection[0];
153-
if (selection && osparc.file.FilesTree.isFile(selection)) {
154-
this.__retrieveURLAndDownloadFile(selection);
156+
if (selection) {
157+
if (osparc.file.FilesTree.isFile(selection)) {
158+
this.__retrieveURLAndDownloadFile(selection);
159+
} else if (isMultiDownloadEnabled) {
160+
const paths = [selection.getPath()];
161+
this.__retrieveURLAndExportData(paths);
162+
}
155163
}
156164
}
157165
},
@@ -167,6 +175,15 @@ qx.Class.define("osparc.file.FileLabelWithActions", {
167175
});
168176
},
169177

178+
__retrieveURLAndExportData: function(paths) {
179+
const dataStore = osparc.store.Data.getInstance();
180+
const fetchPromise = dataStore.exportData(paths);
181+
const pollTasks = osparc.store.PollTasks.getInstance();
182+
pollTasks.createPollingTask(fetchPromise)
183+
.then(task => this.__exportDataTaskReceived(task))
184+
.catch(err => osparc.FlashMessenger.logError(err, this.tr("Unsuccessful files download")));
185+
},
186+
170187
__deleteSelected: function() {
171188
const toBeDeleted = [];
172189
let isFolderSelected = false;
@@ -223,63 +240,127 @@ qx.Class.define("osparc.file.FileLabelWithActions", {
223240
const fetchPromise = dataStore.deleteFiles(paths);
224241
const pollTasks = osparc.store.PollTasks.getInstance();
225242
pollTasks.createPollingTask(fetchPromise)
226-
.then(task => {
227-
const taskUI = new osparc.task.TaskUI();
228-
taskUI.setIcon("@FontAwesome5Solid/trash/14");
229-
taskUI.setTitle(this.tr("Deleting files"));
230-
taskUI.setTask(task);
231-
osparc.task.TasksContainer.getInstance().addTaskUI(taskUI);
232-
233-
const progressWindow = new osparc.ui.window.Progress(
234-
this.tr("Delete files"),
235-
"@FontAwesome5Solid/trash/14",
236-
this.tr("Deleting files..."),
237-
);
238-
if (task.getAbortHref()) {
239-
const cancelButton = progressWindow.addCancelButton();
240-
cancelButton.setLabel(this.tr("Ignore"));
241-
const abortButton = new qx.ui.form.Button().set({
242-
label: this.tr("Cancel"),
243-
center: true,
244-
minWidth: 100,
245-
});
246-
abortButton.addListener("execute", () => task.abortRequested());
247-
progressWindow.addButton(abortButton);
248-
abortButton.set({
249-
appearance: "danger-button",
250-
});
243+
.then(task => this.__deleteTaskReceived(task, paths))
244+
.catch(err => osparc.FlashMessenger.logError(err, this.tr("Unsuccessful files deletion")));
245+
}
246+
},
247+
248+
__exportDataTaskReceived: function(task) {
249+
const exportDataTaskUI = new osparc.task.ExportData();
250+
exportDataTaskUI.setTask(task);
251+
osparc.task.TasksContainer.getInstance().addTaskUI(exportDataTaskUI);
252+
253+
const progressWindow = new osparc.ui.window.Progress(
254+
this.tr("Downloading files"),
255+
"@FontAwesome5Solid/download/14",
256+
this.tr("Downloading files..."),
257+
);
258+
if (task.getAbortHref()) {
259+
const cancelButton = progressWindow.addCancelButton();
260+
cancelButton.setLabel(this.tr("Ignore"));
261+
const abortButton = new qx.ui.form.Button().set({
262+
label: this.tr("Cancel"),
263+
center: true,
264+
minWidth: 100,
265+
});
266+
abortButton.addListener("execute", () => task.abortRequested());
267+
progressWindow.addButton(abortButton);
268+
abortButton.set({
269+
appearance: "danger-button",
270+
});
271+
}
272+
progressWindow.open();
273+
274+
task.addListener("updateReceived", e => {
275+
const data = e.getData();
276+
if (data["task_progress"]) {
277+
if ("message" in data["task_progress"] && data["task_progress"]["message"]) {
278+
progressWindow.setMessage(data["task_progress"]["message"]);
279+
}
280+
progressWindow.setProgress(osparc.data.PollTask.extractProgress(data) * 100);
281+
}
282+
}, this);
283+
task.addListener("resultReceived", e => {
284+
const taskData = e.getData();
285+
if (taskData["result"]) {
286+
const params = {
287+
url: {
288+
locationId: 0,
289+
fileUuid: encodeURIComponent(taskData["result"]),
251290
}
252-
progressWindow.open();
253-
254-
const finished = () => {
255-
progressWindow.close();
256-
};
257-
258-
task.addListener("updateReceived", e => {
259-
const data = e.getData();
260-
if (data["task_progress"]) {
261-
if ("message" in data["task_progress"] && data["task_progress"]["message"]) {
262-
progressWindow.setMessage(data["task_progress"]["message"]);
263-
}
264-
progressWindow.setProgress(osparc.data.PollTask.extractProgress(data) * 100);
291+
};
292+
osparc.data.Resources.fetch("storageLink", "getOne", params)
293+
.then(data => {
294+
if (data && data.link) {
295+
const fileName = taskData["result"].split("/").pop();
296+
osparc.utils.Utils.downloadLink(data.link, "GET", fileName);
265297
}
266-
}, this);
267-
task.addListener("resultReceived", e => {
268-
finished();
269-
osparc.FlashMessenger.logAs(this.tr("Items successfully deleted"), "INFO");
270-
this.fireDataEvent("pathsDeleted", paths);
271-
});
272-
task.addListener("taskAborted", () => {
273-
finished();
274-
osparc.FlashMessenger.logAs(this.tr("Deletion aborted"), "WARNING");
275-
});
276-
task.addListener("pollingError", e => {
277-
const err = e.getData();
278-
osparc.FlashMessenger.logError(err);
279-
});
280-
})
281-
.catch(err => osparc.FlashMessenger.logError(err, this.tr("Unsuccessful files deletion")));
298+
})
299+
}
300+
progressWindow.close();
301+
});
302+
task.addListener("taskAborted", () => {
303+
osparc.FlashMessenger.logAs(this.tr("Download aborted"), "WARNING");
304+
progressWindow.close();
305+
});
306+
task.addListener("pollingError", e => {
307+
const err = e.getData();
308+
osparc.FlashMessenger.logError(err);
309+
progressWindow.close();
310+
});
311+
},
312+
313+
__deleteTaskReceived: function(task, paths) {
314+
const taskUI = new osparc.task.TaskUI();
315+
taskUI.setIcon("@FontAwesome5Solid/trash/14");
316+
taskUI.setTitle(this.tr("Deleting files"));
317+
taskUI.setTask(task);
318+
osparc.task.TasksContainer.getInstance().addTaskUI(taskUI);
319+
320+
const progressWindow = new osparc.ui.window.Progress(
321+
this.tr("Delete files"),
322+
"@FontAwesome5Solid/trash/14",
323+
this.tr("Deleting files..."),
324+
);
325+
if (task.getAbortHref()) {
326+
const cancelButton = progressWindow.addCancelButton();
327+
cancelButton.setLabel(this.tr("Ignore"));
328+
const abortButton = new qx.ui.form.Button().set({
329+
label: this.tr("Cancel"),
330+
center: true,
331+
minWidth: 100,
332+
});
333+
abortButton.addListener("execute", () => task.abortRequested());
334+
progressWindow.addButton(abortButton);
335+
abortButton.set({
336+
appearance: "danger-button",
337+
});
282338
}
339+
progressWindow.open();
340+
341+
task.addListener("updateReceived", e => {
342+
const data = e.getData();
343+
if (data["task_progress"]) {
344+
if ("message" in data["task_progress"] && data["task_progress"]["message"]) {
345+
progressWindow.setMessage(data["task_progress"]["message"]);
346+
}
347+
progressWindow.setProgress(osparc.data.PollTask.extractProgress(data) * 100);
348+
}
349+
}, this);
350+
task.addListener("resultReceived", e => {
351+
osparc.FlashMessenger.logAs(this.tr("Items successfully deleted"), "INFO");
352+
this.fireDataEvent("pathsDeleted", paths);
353+
progressWindow.close();
354+
});
355+
task.addListener("taskAborted", () => {
356+
osparc.FlashMessenger.logAs(this.tr("Deletion aborted"), "WARNING");
357+
progressWindow.close();
358+
});
359+
task.addListener("pollingError", e => {
360+
const err = e.getData();
361+
osparc.FlashMessenger.logError(err);
362+
progressWindow.close();
363+
});
283364
},
284365
}
285366
});

services/static-webserver/client/source/class/osparc/store/Data.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,22 @@ qx.Class.define("osparc.store.Data", {
234234
return true;
235235
},
236236

237+
exportData: function(paths) {
238+
if (!osparc.data.Permissions.getInstance().canDo("study.node.data.delete", true)) {
239+
return null;
240+
}
241+
242+
const params = {
243+
url: {
244+
locationId: 0,
245+
},
246+
data: {
247+
paths,
248+
}
249+
};
250+
return osparc.data.Resources.fetch("storagePaths", "multiDownload", params);
251+
},
252+
237253
deleteFiles: function(paths) {
238254
if (!osparc.data.Permissions.getInstance().canDo("study.node.data.delete", true)) {
239255
return null;

services/static-webserver/client/source/class/osparc/store/StaticInfo.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ qx.Class.define("osparc.store.StaticInfo", {
121121
return wsStaticData[key];
122122
}
123123
return null;
124-
}
124+
},
125+
126+
isDevFeaturesEnabled: function() {
127+
return this.getValue("webserverDevFeaturesEnabled");
128+
},
125129
}
126130
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* ************************************************************************
2+
3+
osparc - the simcore frontend
4+
5+
https://osparc.io
6+
7+
Copyright:
8+
2025 IT'IS Foundation, https://itis.swiss
9+
10+
License:
11+
MIT: https://opensource.org/licenses/MIT
12+
13+
Authors:
14+
* Odei Maiz (odeimaiz)
15+
16+
************************************************************************ */
17+
18+
qx.Class.define("osparc.task.ExportData", {
19+
extend: osparc.task.TaskUI,
20+
21+
construct: function() {
22+
this.base(arguments);
23+
24+
this.setIcon(this.self().ICON+"/14");
25+
this.setTitle(this.tr("Downloading files:"));
26+
},
27+
28+
statics: {
29+
ICON: "@FontAwesome5Solid/download"
30+
},
31+
});

services/static-webserver/client/source/class/osparc/task/TaskUI.js

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -121,35 +121,38 @@ qx.Class.define("osparc.task.TaskUI", {
121121
},
122122

123123
__applyTask: function(task) {
124-
task.addListener("updateReceived", e => {
125-
const data = e.getData();
126-
if (data["task_progress"]) {
127-
if ("message" in data["task_progress"] && !this.getChildControl("subtitle").getValue()) {
128-
this.getChildControl("subtitle").setValue(data["task_progress"]["message"]);
129-
}
130-
this.getChildControl("progress").setValue((osparc.data.PollTask.extractProgress(data) * 100) + "%");
131-
}
132-
}, this);
124+
task.addListener("updateReceived", e => this._updateHandler(e.getData()), this);
133125

134126
const stopButton = this.getChildControl("stop");
135127
task.bind("abortHref", stopButton, "visibility", {
136128
converter: abortHref => abortHref ? "visible" : "excluded"
137129
});
138-
stopButton.addListener("tap", () => {
139-
const msg = this.tr("Are you sure you want to cancel the task?");
140-
const win = new osparc.ui.window.Confirmation(msg).set({
141-
caption: this.tr("Cancel Task"),
142-
confirmText: this.tr("Cancel"),
143-
confirmAction: "delete"
144-
});
145-
win.getCancelButton().setLabel(this.tr("Ignore"));
146-
win.center();
147-
win.open();
148-
win.addListener("close", () => {
149-
if (win.getConfirmed()) {
150-
task.abortRequested();
151-
}
152-
}, this);
130+
stopButton.addListener("tap", () => this._abortHandler(), this);
131+
},
132+
133+
_updateHandler: function(data) {
134+
if (data["task_progress"]) {
135+
if ("message" in data["task_progress"] && !this.getChildControl("subtitle").getValue()) {
136+
this.getChildControl("subtitle").setValue(data["task_progress"]["message"]);
137+
}
138+
this.getChildControl("progress").setValue((osparc.data.PollTask.extractProgress(data) * 100) + "%");
139+
}
140+
},
141+
142+
_abortHandler: function() {
143+
const msg = this.tr("Are you sure you want to cancel the task?");
144+
const win = new osparc.ui.window.Confirmation(msg).set({
145+
caption: this.tr("Cancel Task"),
146+
confirmText: this.tr("Cancel"),
147+
confirmAction: "delete"
148+
});
149+
win.getCancelButton().setLabel(this.tr("Ignore"));
150+
win.center();
151+
win.open();
152+
win.addListener("close", () => {
153+
if (win.getConfirmed()) {
154+
this.getTask().abortRequested();
155+
}
153156
}, this);
154157
},
155158

0 commit comments

Comments
 (0)