From 757a1fb953ad7655a0cb87eaa485c6b882a495f7 Mon Sep 17 00:00:00 2001 From: Niklas Edmundsson Date: Fri, 12 May 2023 15:55:56 +0200 Subject: [PATCH] Let browser handle downloads directly Currently dCacheView handles downloads by first doing the download and only when the download has completed it passes the object along for the browser to handle. The result is no feedback at all for users when initiating download of large files since the save dialog is shown on completion, and huge files might not download at all if the temporary browser download location runs out of space. Work around this by letting the browser handle the download instead, the method chosen is to create a temporary Anchor element to drive the download action as it has an explicit download attribute and avoids issues with modern browser pop-up blockers. Since username/password (Basic) auth can't be reliably passed along a short-lived Macaroon is created to handle the download. Existing Macaroons are used as-is to handle download in the shared-files view. Sessions with certificate authentication are assumed to work as-is, bypassing the Macaroon generation stage. The result is a decent end user experience when downloading files, including huge files that are common in a scientific data store. Credits go to various threads on https://stackoverflow.com/ for explaining numerous corner cases and nuances in this area. Fixes: https://github.com/dCache/dcache-view/issues/269 Signed-off-by: Niklas Edmundsson --- src/scripts/dv.js | 74 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/src/scripts/dv.js b/src/scripts/dv.js index f60924d..b4d930c 100644 --- a/src/scripts/dv.js +++ b/src/scripts/dv.js @@ -501,6 +501,14 @@ .set(`items.${itemIndex}.currentQos`, status); vf.shadowRoot.querySelector('#feList').notifyPath(`items.${itemIndex}.currentQos`); } + /* Initiate browser file download */ + function _downloadFile(url) + { + var dl = document.createElement("a"); + dl.setAttribute('href', url); + dl.setAttribute('download', ''); + dl.click(); + } window.addEventListener('qos-in-transition', function(event) { updateFeListAndMetaDataDrawer([`${event.detail.options.targetQos}`], event.detail.options.itemIndex); @@ -632,32 +640,52 @@ app.ls(e.detail.file.filePath, auth); Polymer.dom.flush(); } else { - //Download the file - const worker = new Worker('./scripts/tasks/download-task.js'); + // Download the file const fileURL = getFileWebDavUrl(e.detail.file.filePath, "read")[0]; - worker.addEventListener('message', (response) => { - worker.terminate(); - const windowUrl = window.URL || window.webkitURL; - const url = windowUrl.createObjectURL(response.data); - const link = app.$.download; - link.href = url; - link.download = e.detail.file.fileMetaData.fileName; - link.click(); - windowUrl.revokeObjectURL(url); - - }, false); - worker.addEventListener('error', (err)=> { - worker.terminate(); - openToast(`${err.message}`); - }, false); - worker.postMessage({ - 'url' : fileURL, - 'mime' : e.detail.file.fileMetaData.fileMimeType, - 'upauth' : app.getAuthValue(auth), - 'return': 'blob' - }); + 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, + }); + } } }); + window.addEventListener('dv-namespace-open-subcontextmenu', e => app.subContextMenu(e)); window.addEventListener('dv-namespace-close-subcontextmenu', () => { app.$.centralSubContextMenu.close();