Skip to content

Commit

Permalink
Let browser handle downloads directly - part 2(2)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
ZNikke committed Dec 5, 2023
1 parent d7b5d8b commit 17161f8
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -642,9 +647,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', "");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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);
window.customElements.define(SharedFilesPage.is, SharedFilesPage);
2 changes: 1 addition & 1 deletion src/elements/dv-elements/files-viewer/files-viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,4 @@
}
window.customElements.define(FilesViewer.is, FilesViewer);
</script>
</dom-module>
</dom-module>
149 changes: 99 additions & 50 deletions src/scripts/dv.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,14 +501,100 @@
.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");
dl.setAttribute('href', url);
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);
Expand Down Expand Up @@ -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");
}
});

Expand Down

0 comments on commit 17161f8

Please sign in to comment.