From bb332f3d6357419e8a813509c3029b54ac3134fe Mon Sep 17 00:00:00 2001 From: Niklas Edmundsson Date: Tue, 16 May 2023 13:40:53 +0200 Subject: [PATCH] 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: https://github.com/dCache/dcache-view/issues/268 Signed-off-by: Niklas Edmundsson --- .../namespace-contextual-content.html | 9 +- .../shared-files-page/shared-files-page.js | 30 +--- .../files-viewer/files-viewer.html | 2 +- src/scripts/dv.js | 149 ++++++++++++------ 4 files changed, 110 insertions(+), 80 deletions(-) diff --git a/src/elements/dv-elements/contextual-content/namespace-contextual-content.html b/src/elements/dv-elements/contextual-content/namespace-contextual-content.html index 30dce4d1..86504188 100644 --- a/src/elements/dv-elements/contextual-content/namespace-contextual-content.html +++ b/src/elements/dv-elements/contextual-content/namespace-contextual-content.html @@ -246,6 +246,10 @@ } else if (this.multipleSelection) { this.$.delete.addEventListener('tap', this._delete.bind(this)); this.$.move.addEventListener('tap', this._move.bind(this)); + // FIXME: Is this the place to figure out if any + // directories are selected and disable download if + // that's the case? + this.$.download.addEventListener('tap', this._openOrDownload.bind(this)); } else if (this.currentDir) { this.$.create.addEventListener('tap', this._create.bind(this)); this.$.metadata.addEventListener('tap', this._metadata.bind(this)); @@ -289,6 +293,7 @@ } else if (this.multipleSelection) { this.$.delete.removeEventListener('tap', this._delete.bind(this)); this.$.move.removeEventListener('tap', this._move.bind(this)); + this.$.download.removeEventListener('tap', this._openOrDownload.bind(this)); } else if (this.currentDir) { this.$.create.removeEventListener('tap', this._create.bind(this)); this.$.metadata.removeEventListener('tap', this._metadata.bind(this)); @@ -641,9 +646,11 @@ _setDisabledAttribute(id, singleSelection, multipleSelection, t) { + // FIXME: Is this the place to figure out if any directories + // are selected and disable download if that's the case? if (multipleSelection && (id === 'open' || id === 'share' || id === 'metadata' || id === 'webdavUrl' || id === 'rename' || id === 'setLabel' || - id === 'download' || id === 'changeQos' || id === 'qosInfo')) { + id === 'changeQos' || id === 'qosInfo')) { this.$[id].setAttribute('disabled', ""); } diff --git a/src/elements/dv-elements/file-sharing/shared-files-page/shared-files-page.js b/src/elements/dv-elements/file-sharing/shared-files-page/shared-files-page.js index 0cc898df..10103763 100644 --- a/src/elements/dv-elements/file-sharing/shared-files-page/shared-files-page.js +++ b/src/elements/dv-elements/file-sharing/shared-files-page/shared-files-page.js @@ -45,33 +45,7 @@ class SharedFilesPage extends DcacheViewMixins.Commons(Polymer.Element) }; this.$['shared-directory-view'].path = e.detail.file.filePath; } else if (e.detail.file.fileMetaData.fileType === "REGULAR") { - //download - const worker = new Worker('./scripts/tasks/download-task.js'); - const fileURL = this.getFileWebDavUrl(e.detail.file.filePath, "read")[0]; - worker.addEventListener('message', (file) => { - worker.terminate(); - const windowUrl = window.URL || window.webkitURL; - const url = windowUrl.createObjectURL(file.data); - const link = app.$.download; - link.href = url; - link.download = e.detail.file.fileMetaData.fileName; - link.click(); - windowUrl.revokeObjectURL(url); - }, false); - worker.addEventListener('error', (e)=> { - console.info(e); - worker.terminate(); - this.dispatchEvent(new CustomEvent('dv-namespace-show-message-toast', { - detail: {message: e.message}, bubbles: true, composed: true - })); - }, false); - this.authenticationParameters = {"scheme": "Bearer", "value": e.detail.file.macaroon}; - worker.postMessage({ - 'url' : fileURL, - 'mime' : e.detail.file.fileMetaData.fileMimeType, - 'upauth' : this.getAuthValue(), - 'return': 'blob' - }); + app._initiateDownload(e.detail.file); } } _showSharedFileList() @@ -81,4 +55,4 @@ class SharedFilesPage extends DcacheViewMixins.Commons(Polymer.Element) this.$['shared-directory-view'].classList.replace('normal', 'none'); } } -window.customElements.define(SharedFilesPage.is, SharedFilesPage); \ No newline at end of file +window.customElements.define(SharedFilesPage.is, SharedFilesPage); diff --git a/src/elements/dv-elements/files-viewer/files-viewer.html b/src/elements/dv-elements/files-viewer/files-viewer.html index 71f350f7..f1f3a3d6 100644 --- a/src/elements/dv-elements/files-viewer/files-viewer.html +++ b/src/elements/dv-elements/files-viewer/files-viewer.html @@ -236,4 +236,4 @@ } window.customElements.define(FilesViewer.is, FilesViewer); - \ No newline at end of file + diff --git a/src/scripts/dv.js b/src/scripts/dv.js index b4d930cc..456a3531 100644 --- a/src/scripts/dv.js +++ b/src/scripts/dv.js @@ -501,7 +501,7 @@ .set(`items.${itemIndex}.currentQos`, status); vf.shadowRoot.querySelector('#feList').notifyPath(`items.${itemIndex}.currentQos`); } - /* Initiate browser file download */ + /* Start browser file download of a URL */ function _downloadFile(url) { var dl = document.createElement("a"); @@ -509,6 +509,92 @@ dl.setAttribute('download', ''); dl.click(); } + /* Initiate download of a single file object */ + app._initiateDownload = function(file) + { + const fileURL = getFileWebDavUrl(file.filePath, "read")[0]; + let authval = app.getAuthValue(); + + // Unconditionally use existing macaroon if available + let macaroon = undefined; + if (file.macaroon) { + macaroon = file.macaroon; + } + else if(file.authenticationParameters !== undefined && file.authenticationParameters.scheme === "Bearer") { + macaroon = file.authenticationParameters.value; + } + + if(macaroon !== undefined) { + let u = new URL(fileURL); + u.searchParams.append('authz', macaroon); + _downloadFile(u); + } + else if(!authval) { + /* + * No explicit auth, so using cert auth, which means we can + * just access the file directly without having the user + * re-login. + */ + _downloadFile(fileURL); + } + else { + /* + * We don't seem to be able to pass our current auth + * via a standard method that triggers the browser standard + * file-download handling, so need to create a short-lived + * Macaroon for it. + */ + const macaroonWorker = new Worker('./scripts/tasks/macaroon-request-task.js'); + macaroonWorker.addEventListener('message', (e) => { + macaroonWorker.terminate(); + _downloadFile(e.data.uri.targetWithMacaroon); + }, false); + macaroonWorker.addEventListener('error', (e) => { + macaroonWorker.terminate(); + // FIXME: Display an error dialog somehow + console.error(e); + }, false); + macaroonWorker.postMessage({ + "url": fileURL, + "body": { + "caveats": ["activity:DOWNLOAD"], + "validity": "PT10M" + }, + 'upauth' : authval, + }); + } + } + /* Initiate downloads of a multi-file selection */ + function _initiateMultiDownload(e) + { + /* For some reason we only have the fileMetaData, so need to + * fabricate the expected structure... + */ + const toDL = []; + for(const f of e.detail.file.files) { + if(f.fileType === "REGULAR") { + let n = {}; + n.fileMetaData = f; + n.filePath = e.target.currentPath.endsWith('/') ? `${e.target.currentPath}${f.fileName}`: `${e.target.currentPath}/${f.fileName}`; + if(e.detail.file.authenticationParameters !== undefined) { + n.authenticationParameters = e.detail.file.authenticationParameters; + } + toDL.push(n); + } + else { + /* FIXME: Either disable download choice if non-file + * selected, or abort and display an error to the user. + */ + console.error(`Skipping ${f.fileType} ${f.fileName}`); + } + } + for (let i = 0; i < toDL.length; i++) { + /* Need to stagger starts to allow browser to start + * this download before initiating the next one. + */ + setTimeout(app._initiateDownload, i*1000, toDL[i]); + } + } window.addEventListener('qos-in-transition', function(event) { updateFeListAndMetaDataDrawer([`${event.detail.options.targetQos}`], event.detail.options.itemIndex); @@ -632,57 +718,20 @@ app.drop(e); }); window.addEventListener('dv-namespace-open-file', function (e) { - let auth; - if (e.detail.file.authenticationParameters !== undefined) { - auth = e.detail.file.authenticationParameters; - } - if (e.detail.file.fileMetaData.fileType === "DIR") { + if (e.detail.file.fileMetaData !== undefined && e.detail.file.fileMetaData.fileType === "DIR") { + let auth; + if (e.detail.file.authenticationParameters !== undefined) { + auth = e.detail.file.authenticationParameters; + } app.ls(e.detail.file.filePath, auth); Polymer.dom.flush(); - } else { - // Download the file - const fileURL = getFileWebDavUrl(e.detail.file.filePath, "read")[0]; - let authval = app.getAuthValue(); - if (e.detail.file.macaroon) { - // Unconditionally use existing macaroon if available - let u = new URL(fileURL); - u.searchParams.append('authz', e.detail.file.macaroon); - _downloadFile(u); - } - else if(!authval) { - /* - * No explicit auth, so using cert auth, which means we can - * just access the file directly without having the user - * re-login. - */ - _downloadFile(fileURL); - } - else { - /* - * We don't seem to be able to pass our current auth - * via a standard method that triggers the browser standard - * file-download handling, so need to create a short-lived - * Macaroon for it. - */ - const macaroonWorker = new Worker('./scripts/tasks/macaroon-request-task.js'); - macaroonWorker.addEventListener('message', (e) => { - macaroonWorker.terminate(); - _downloadFile(e.data.uri.targetWithMacaroon); - }, false); - macaroonWorker.addEventListener('error', (e) => { - macaroonWorker.terminate(); - // FIXME: Display an error dialog somehow - console.error(e); - }, false); - macaroonWorker.postMessage({ - "url": fileURL, - "body": { - "caveats": ["activity:DOWNLOAD"], - "validity": "PT1M" - }, - 'upauth' : authval, - }); - } + } else if (e.target.__data.singleSelection === true) { + app._initiateDownload(e.detail.file); + } else if (e.target.__data.multipleSelection === true) { + _initiateMultiDownload(e); + } + else { + console.error("Internal error, no matching state"); } });