Skip to content

Commit 17161f8

Browse files
committed
Let browser handle downloads directly - part 2(2)
dCacheView is limited to file-by-file downloads, which is frustrating users since there is support for multi-file selection. This patch builds on the previous one to enable downloads for multi-file selections. The simplest solution is implemented that triggers multiple file downloads in the browser, this is also found to work best with tablets and smartphones, where the traditional method by handling multi-file download by providing a .zip archive of the files is really cumbersome to handle. There is also a completion of the implementation to handle sub-directories in the shared-files view. The end result is that it is now possible to select multiple files in a directory and then download them in a smooth manner. Fixes: #268 Signed-off-by: Niklas Edmundsson <nikke@hpc2n.umu.se>
1 parent d7b5d8b commit 17161f8

4 files changed

Lines changed: 110 additions & 80 deletions

File tree

src/elements/dv-elements/contextual-content/namespace-contextual-content.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@
246246
} else if (this.multipleSelection) {
247247
this.$.delete.addEventListener('tap', this._delete.bind(this));
248248
this.$.move.addEventListener('tap', this._move.bind(this));
249+
// FIXME: Is this the place to figure out if any
250+
// directories are selected and disable download if
251+
// that's the case?
252+
this.$.download.addEventListener('tap', this._openOrDownload.bind(this));
249253
} else if (this.currentDir) {
250254
this.$.create.addEventListener('tap', this._create.bind(this));
251255
this.$.metadata.addEventListener('tap', this._metadata.bind(this));
@@ -289,6 +293,7 @@
289293
} else if (this.multipleSelection) {
290294
this.$.delete.removeEventListener('tap', this._delete.bind(this));
291295
this.$.move.removeEventListener('tap', this._move.bind(this));
296+
this.$.download.removeEventListener('tap', this._openOrDownload.bind(this));
292297
} else if (this.currentDir) {
293298
this.$.create.removeEventListener('tap', this._create.bind(this));
294299
this.$.metadata.removeEventListener('tap', this._metadata.bind(this));
@@ -642,9 +647,11 @@
642647

643648
_setDisabledAttribute(id, singleSelection, multipleSelection, t)
644649
{
650+
// FIXME: Is this the place to figure out if any directories
651+
// are selected and disable download if that's the case?
645652
if (multipleSelection && (id === 'open' || id === 'share' ||
646653
id === 'metadata' || id === 'webdavUrl' || id === 'rename' || id === 'setLabel' ||
647-
id === 'download' || id === 'changeQos' || id === 'qosInfo')) {
654+
id === 'changeQos' || id === 'qosInfo')) {
648655
this.$[id].setAttribute('disabled', "");
649656
}
650657

src/elements/dv-elements/file-sharing/shared-files-page/shared-files-page.js

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,33 +45,7 @@ class SharedFilesPage extends DcacheViewMixins.Commons(Polymer.Element)
4545
};
4646
this.$['shared-directory-view'].path = e.detail.file.filePath;
4747
} else if (e.detail.file.fileMetaData.fileType === "REGULAR") {
48-
//download
49-
const worker = new Worker('./scripts/tasks/download-task.js');
50-
const fileURL = this.getFileWebDavUrl(e.detail.file.filePath, "read")[0];
51-
worker.addEventListener('message', (file) => {
52-
worker.terminate();
53-
const windowUrl = window.URL || window.webkitURL;
54-
const url = windowUrl.createObjectURL(file.data);
55-
const link = app.$.download;
56-
link.href = url;
57-
link.download = e.detail.file.fileMetaData.fileName;
58-
link.click();
59-
windowUrl.revokeObjectURL(url);
60-
}, false);
61-
worker.addEventListener('error', (e)=> {
62-
console.info(e);
63-
worker.terminate();
64-
this.dispatchEvent(new CustomEvent('dv-namespace-show-message-toast', {
65-
detail: {message: e.message}, bubbles: true, composed: true
66-
}));
67-
}, false);
68-
this.authenticationParameters = {"scheme": "Bearer", "value": e.detail.file.macaroon};
69-
worker.postMessage({
70-
'url' : fileURL,
71-
'mime' : e.detail.file.fileMetaData.fileMimeType,
72-
'upauth' : this.getAuthValue(),
73-
'return': 'blob'
74-
});
48+
app._initiateDownload(e.detail.file);
7549
}
7650
}
7751
_showSharedFileList()
@@ -81,4 +55,4 @@ class SharedFilesPage extends DcacheViewMixins.Commons(Polymer.Element)
8155
this.$['shared-directory-view'].classList.replace('normal', 'none');
8256
}
8357
}
84-
window.customElements.define(SharedFilesPage.is, SharedFilesPage);
58+
window.customElements.define(SharedFilesPage.is, SharedFilesPage);

src/elements/dv-elements/files-viewer/files-viewer.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,4 +236,4 @@
236236
}
237237
window.customElements.define(FilesViewer.is, FilesViewer);
238238
</script>
239-
</dom-module>
239+
</dom-module>

src/scripts/dv.js

Lines changed: 99 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -501,14 +501,100 @@
501501
.set(`items.${itemIndex}.currentQos`, status);
502502
vf.shadowRoot.querySelector('#feList').notifyPath(`items.${itemIndex}.currentQos`);
503503
}
504-
/* Initiate browser file download */
504+
/* Start browser file download of a URL */
505505
function _downloadFile(url)
506506
{
507507
var dl = document.createElement("a");
508508
dl.setAttribute('href', url);
509509
dl.setAttribute('download', '');
510510
dl.click();
511511
}
512+
/* Initiate download of a single file object */
513+
app._initiateDownload = function(file)
514+
{
515+
const fileURL = getFileWebDavUrl(file.filePath, "read")[0];
516+
let authval = app.getAuthValue();
517+
518+
// Unconditionally use existing macaroon if available
519+
let macaroon = undefined;
520+
if (file.macaroon) {
521+
macaroon = file.macaroon;
522+
}
523+
else if(file.authenticationParameters !== undefined && file.authenticationParameters.scheme === "Bearer") {
524+
macaroon = file.authenticationParameters.value;
525+
}
526+
527+
if(macaroon !== undefined) {
528+
let u = new URL(fileURL);
529+
u.searchParams.append('authz', macaroon);
530+
_downloadFile(u);
531+
}
532+
else if(!authval) {
533+
/*
534+
* No explicit auth, so using cert auth, which means we can
535+
* just access the file directly without having the user
536+
* re-login.
537+
*/
538+
_downloadFile(fileURL);
539+
}
540+
else {
541+
/*
542+
* We don't seem to be able to pass our current auth
543+
* via a standard method that triggers the browser standard
544+
* file-download handling, so need to create a short-lived
545+
* Macaroon for it.
546+
*/
547+
const macaroonWorker = new Worker('./scripts/tasks/macaroon-request-task.js');
548+
macaroonWorker.addEventListener('message', (e) => {
549+
macaroonWorker.terminate();
550+
_downloadFile(e.data.uri.targetWithMacaroon);
551+
}, false);
552+
macaroonWorker.addEventListener('error', (e) => {
553+
macaroonWorker.terminate();
554+
// FIXME: Display an error dialog somehow
555+
console.error(e);
556+
}, false);
557+
macaroonWorker.postMessage({
558+
"url": fileURL,
559+
"body": {
560+
"caveats": ["activity:DOWNLOAD"],
561+
"validity": "PT10M"
562+
},
563+
'upauth' : authval,
564+
});
565+
}
566+
}
567+
/* Initiate downloads of a multi-file selection */
568+
function _initiateMultiDownload(e)
569+
{
570+
/* For some reason we only have the fileMetaData, so need to
571+
* fabricate the expected structure...
572+
*/
573+
const toDL = [];
574+
for(const f of e.detail.file.files) {
575+
if(f.fileType === "REGULAR") {
576+
let n = {};
577+
n.fileMetaData = f;
578+
n.filePath = e.target.currentPath.endsWith('/') ? `${e.target.currentPath}${f.fileName}`: `${e.target.currentPath}/${f.fileName}`;
579+
if(e.detail.file.authenticationParameters !== undefined) {
580+
n.authenticationParameters = e.detail.file.authenticationParameters;
581+
}
582+
toDL.push(n);
583+
}
584+
else {
585+
/* FIXME: Either disable download choice if non-file
586+
* selected, or abort and display an error to the user.
587+
*/
588+
console.error(`Skipping ${f.fileType} ${f.fileName}`);
589+
}
590+
}
591+
for (let i = 0; i < toDL.length; i++) {
592+
/* Need to stagger starts to allow browser to start
593+
* this download before initiating the next one.
594+
*/
595+
setTimeout(app._initiateDownload, i*1000, toDL[i]);
596+
}
597+
}
512598

513599
window.addEventListener('qos-in-transition', function(event) {
514600
updateFeListAndMetaDataDrawer([`${event.detail.options.targetQos}`], event.detail.options.itemIndex);
@@ -632,57 +718,20 @@
632718
app.drop(e);
633719
});
634720
window.addEventListener('dv-namespace-open-file', function (e) {
635-
let auth;
636-
if (e.detail.file.authenticationParameters !== undefined) {
637-
auth = e.detail.file.authenticationParameters;
638-
}
639-
if (e.detail.file.fileMetaData.fileType === "DIR") {
721+
if (e.detail.file.fileMetaData !== undefined && e.detail.file.fileMetaData.fileType === "DIR") {
722+
let auth;
723+
if (e.detail.file.authenticationParameters !== undefined) {
724+
auth = e.detail.file.authenticationParameters;
725+
}
640726
app.ls(e.detail.file.filePath, auth);
641727
Polymer.dom.flush();
642-
} else {
643-
// Download the file
644-
const fileURL = getFileWebDavUrl(e.detail.file.filePath, "read")[0];
645-
let authval = app.getAuthValue();
646-
if (e.detail.file.macaroon) {
647-
// Unconditionally use existing macaroon if available
648-
let u = new URL(fileURL);
649-
u.searchParams.append('authz', e.detail.file.macaroon);
650-
_downloadFile(u);
651-
}
652-
else if(!authval) {
653-
/*
654-
* No explicit auth, so using cert auth, which means we can
655-
* just access the file directly without having the user
656-
* re-login.
657-
*/
658-
_downloadFile(fileURL);
659-
}
660-
else {
661-
/*
662-
* We don't seem to be able to pass our current auth
663-
* via a standard method that triggers the browser standard
664-
* file-download handling, so need to create a short-lived
665-
* Macaroon for it.
666-
*/
667-
const macaroonWorker = new Worker('./scripts/tasks/macaroon-request-task.js');
668-
macaroonWorker.addEventListener('message', (e) => {
669-
macaroonWorker.terminate();
670-
_downloadFile(e.data.uri.targetWithMacaroon);
671-
}, false);
672-
macaroonWorker.addEventListener('error', (e) => {
673-
macaroonWorker.terminate();
674-
// FIXME: Display an error dialog somehow
675-
console.error(e);
676-
}, false);
677-
macaroonWorker.postMessage({
678-
"url": fileURL,
679-
"body": {
680-
"caveats": ["activity:DOWNLOAD"],
681-
"validity": "PT1M"
682-
},
683-
'upauth' : authval,
684-
});
685-
}
728+
} else if (e.target.__data.singleSelection === true) {
729+
app._initiateDownload(e.detail.file);
730+
} else if (e.target.__data.multipleSelection === true) {
731+
_initiateMultiDownload(e);
732+
}
733+
else {
734+
console.error("Internal error, no matching state");
686735
}
687736
});
688737

0 commit comments

Comments
 (0)