Reputation: 1772
I am working on a code base where I need to allow the user to download a PDF document that already resides on AWS S3. I have implemented a download concern that was used for a previous feature.
For this feature, I need to update the UI (A progress stepper) after the user has completed the file download. I was initially thinking that this would be as simple as:
send_data
. In this API call, I'd also update the Foo
model to change state to indicate that the user has downloaded the file;redirect_to request.referer
to reload the data. The changed state in Foo
will be responsible for showing the updated progress in the UI;I was mistakenly thinking that this was going to be simple. The reasons for complexity:
send_data
is already rendering data, so I can't refresh the page using redirect_to
as this triggers a multiple render error;send_data
does not work with the remote: true
option, so requesting data via an AJAX link and updating the ERB template is out;on click
function, but this seems like a bit of a hack. I probably need to retrieve the file directly from AWS and skip my api? I'm suspecting that I might run into CORS issues as I don't have control over the server.This is what my rails download method looks like currently:
def download
attachment = Attachment.find_by_id(params[:attachment_id])
content = send_data(
attachment.file.read,
filename: "#{attachment.title}.#{attachment.file.file.extension}",
type: attachment.content_type,
disposition: "attachment",
)
end
Th js code that basically worked looks like this where all the relevant paths & filenames are passed on to the JS via data-attributes:
$(document).on("click", "#download", function(e){
e.preventDefault();
const data = $('#temp-information').data();
var req = new XMLHttpRequest();
req.open("GET", data.path, true);
req.responseType = "blob";
const filename = data.title;
req.onload = function (event) {
var blob = req.response;
console.log(blob.size);
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download= filename;
document.body.appendChild(link);
link.click();
};
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// Fix to work in IE11
window.navigator.msSaveBlob(blob, filename);
} else {
req.send();
}
});
What is the most effective & rails'y way of handling a file download & updating the UI after the download has been completed?
Upvotes: 4
Views: 3465
Reputation: 43113
It's not 100% clear what you're trying to accomplish. If you're trying to let the user see download progress, I'm not sure that you really need to do anything except send_data
, and most browsers will then begin downloading the file, including showing a progress bar.
Since it seems you want to do something after the file download is complete, that's quite a bit trickier. There's nothing Rails-specific about the problem, and the approach you have used looks pretty reasonable to me.
On this SO thread you'll find a lengthy discussion of this problem and various ways that people have tried to solve it. In general the solutions follow the same basic structure, which is to simply poll the server.
In your Rails app you could implement that roughly as follows. Suppose you added a field status
to your attachment model...
def download
attachment = Attachment.find_by_id(params[:attachment_id])
attachment.update(status: "downloading")
send_data(
attachment.file.read,
filename: "#{attachment.title}.#{attachment.file.file.extension}",
type: attachment.content_type,
disposition: "attachment",
)
attachment.update(status: "complete")
end
Then you can add an endpoint that returns the status of a file. Thus when the user starts to download the file you begin to poll that endpoint.
def attachment_status
attachment = Attachment.find_by_id(params[:attachment_id])
respond_to do |format|
format.json do
{status: attachment.status}
end
end
end
Then in Javascript, for example using HttpPromise:
var http = new HttpPromise;
function poll(doneFn) {
http.get("/status.json") // you will need to set your actual status endpoint path here
.success(function(data,xhr){
if (data.status == "complete") {
doneFn();
}
});
};
function downloadFinished(){
// ... do whatever you want on finish here ...
};
setInterval(function(){ poll(downloadFinished) }, 5000);
It's not the most beautiful thing in the world, but it should get the job done.
Good luck!
Upvotes: 1