When files are downloaded with the fileRef.link() they will be named after the file on the server. However, I would like to "rename" the file when downloaded. I tried the a[download] attribute, but that doesn't seem to work even though the files are downloaded from the same origin.
I'm using React and I would like to do something like:
<Button size="small" color="primary" href={this.props.video.link() + '?download=true'} download="custom-name.mp4">Download</Button>
The filename in the systems save dialog should then default to custom-name.mp4.
Any ideas how to achieve that?
I found a workaround using the interceptDownload and then checking for a specific version prefix renamed-download. If that prefix is present, I'll load the file from the file system and pipe the result back to the client, but before that, I change the filename with the Content-Disposition response header.
Here it is:
recordings.js with the collection and the interceptDownload function
const Recordings = new FilesCollection({
collectionName: 'Recordings',
storagePath: config.uploadPath,
allowClientCode: false, // Disallow remove files from client
interceptDownload(http, fileRef, version) {
// continue with default behavior if not requested renamed-download
if (!version.startsWith("renamed-download")) return false;
// get new filename from version name
const downloadFileName = version.substring("renamed-download".length + 1, version.length);
// set new response header that renames the file
const response = http.response;
response.setHeader('Content-Disposition', `inline; filename="${downloadFileName}"`);
// load file and pipe it to user, return true is required to bypass default download option
const readStream = fs.createReadStream(fileRef.path);
readStream.on('error', err => { throw err; });
readStream.pipe(response);
return true;
}
});
Then on the client, I do:
<Button size="small" color="primary" href={this.props.video.link("renamed-download=my-new-filename.mp4") + '?download=true'}>Download</Button>
I know it's a workaround and something like the following example might be better:
<Button size="small" color="primary" href={this.props.video.link() + '?download=my-new-filename.mp4'}>Download</Button>
Hello @raedle it can be simply accomplished by using fileName option passed into .insert() method.
I tried the a[download] attribute, but that doesn't seem to work even though the files are downloaded from the same origin.
It works, but only on __some__ browsers
Thanks @dr-dimitru!
Unfortunately, the filename required when downloading a file is unknown at the time of the upload/insert.
So, the insert wouldn't work anyway
@raedle
Unfortunately, the filename required when downloading a file is unknown at the time of the upload/insert.
This explains a lot.
In your specific case the solution you've chosen is the only right way to accomplish it 馃憤
Here is my recommendations/improvements over your solution to properly serve file name in all browsers:
const downloadFileName = version.substring("renamed-download".length + 1, version.length);
const dispositionType = 'attachment; ';
const dispositionName = `filename=\"${encodeURI(downloadFileName).replace(/\,/g, '%2C')}\"; filename*=UTF-8''${encodeURIComponent(downloadFileName)}; `;
const dispositionEncoding = 'charset=UTF-8';
if (!http.response.headersSent) {
http.response.setHeader('Content-Disposition', dispositionType + dispositionName + dispositionEncoding);
}
Great! Thank you for your improvements.
Also, how do you feel about an additional parameter on the link to make it easier to customize the download filename?
For example:
<a href={fileRef.link() + '?download=my-new-filename.mp4'}>Download</a>
or
<a href={fileRef.link() + '?download=true&downloadFileName=my-new-filename.mp4'}>Download</a>
We see no need to change current API.
at the same time you can easily add support for it in your current implementation, via reading value of http.params.query.download.
Even better. Thanks!
It's now like this:
const Recordings = new FilesCollection({
collectionName: 'Recordings',
storagePath: config.uploadPath,
allowClientCode: false, // Disallow remove files from client
interceptDownload(http, fileRef, version) {
// continue with default behavior if download not requested with filename query param
if (!http.params.query.filename) return false;
// get filename from query
const filename = http.params.query.filename;
const dispositionType = 'attachment; ';
const dispositionName = `filename="${encodeURI(filename).replace(/\\,/g, '%2C')}"; filename*=UTF-8''${encodeURIComponent(filename)}; `;
const dispositionEncoding = 'charset=UTF-8';
// add response header to name file on client side if headers not already sent
if (!http.response.headersSent) {
http.response.setHeader('Content-Disposition', dispositionType + dispositionName + dispositionEncoding);
}
// load file and pipe it to user, return true is required to bypass default download option
const readStream = fs.createReadStream(fileRef.versions[version].path);
readStream.on('error', (err) => { throw err; });
readStream.pipe(http.response);
return true;
}
});
<Button size="small" color="primary" href={this.props.video.link() + `?download=true&filename=${this.state.downloadFileName}`}>Download</Button>
@dr-dimitru Further comments?
And thank you for providing the library. It's great and configurable!
@raedle looks nice.
Just to stay on the safe side, pass filename query string through decodeURIComponent, otherwise it may be encoded twice.
Feel free to close it in case if the issue is solved on your end.
Yes, the problem is solved. Thank you, Dimitru for your quick response. That's very rare and thus very much appreciated!
@raedle my pleasure :) Thank you for choosing our lib.
Please, support this project by: