So I will explain the problem:
Steps:
1) client (browser javascript) sends an Ajax request to the server that hits a controller method called download.
2) the controller's method creates a PDF resource(without saving on the filesystem), and returns a response with the PDF binary stream back to the client.
3) the client receives the PDF binary stream and download it on the client's computer. Is that possible?
Code:
Things I have already tried -
Client-side:
<script>
(function($) {
var button; // some random DOM button
button.on('click', function(e) {
e.preventDefault();
$.ajax({
url: "/download/:userId"
method: "POST",
dataType: "json"
success: function(response) {
var reader = new FileReader;
var file = new Blob([response.pdf_stream], 'application/pdf');
// create a generic download link
var a = $('<a/>', {
href: file,
download: response.filename
});
// trigger click event on that generic link.
a.get(0).click();
}
});
}
})(jQuery);
</script>
On the server-side:
class Controller
{
public function download($userId)
{
// fetching the user from the database
$user = User::find($userId);
// creating a pdf file using barry pdfdom package
// this will actually parse an HTML view and give us the PDF blob.
$pdf = PDF::loadView('pdf.view')->output();
// using Laravel helper function
return response()->json([
'pdf_stream' => utf8_encode($pdf),
'filename' => 'blahblah.pdf"
]);
// Or if you will in native PHP, just in case you don't use laravel.
echo json_encode([
'pdf_stream' => utf8_encode($pdf),
'filename' => 'blahblah.pdf"
]);
}
}
Any Idea what am I doing wrong here? How could I download that PDF file without saving it to the system (security and space concerns).
Any help would be appreciated.
Eden
If you want download pdf on client side, just open this pdf in new window. Use GET request for that things, like in RESTfull application (e.g. download/user/:id or somehow like that).
Could be useful:
Download and open pdf file using Ajax
The main problem is the returned response from controller. Try this:
public function download($userId)
{
// fetching the user from the database
$user = User::find($userId);
// creating a pdf file using barry pdfdom package
// this will actually parse an HTML view and give us the PDF blob.
$pdf = PDF::loadView('pdf.view')->output();
return response($pdf, 200,
[
'Content-Type' => 'application/pdf',
'Content-Length' => strlen($pdf),
'Cache-Control' => 'private, max-age=0, must-revalidate',
'Pragma' => 'public'
]
);
About calling the route which executes download($userid) method:
You do not have to use Ajax. Easy way:
Click view PDF
Related
I am using Chrome and trying to upload a file from browser client. My API given below.
[HttpPost("attachment/{confirmId:long}")]
public async Task<IActionResult> UploadAttachmentAsync(long confirmId, IFormFile filePayload)
{
// ...
}
This works in Swagger but when I am trying from Javascript client, filePayload is null. The confirmId param comes through fine.
I used HttpToolkit and verified that the content of the file is there.
However, this is my client code anyway....
async uploadAttachmentAsync(file, confirmId) {
// buildUrl is my own utility, creates url correctly
const url = buildUrl(`paperConfirm/attachment/${confirmId}`);
const body = new FormData();
body.append('file', file);
// XH is a 3rd party library. I am confident there is no problem there
return await XH.fetchJson({
url,
body,
method: 'POST',
// Note: We must explicitly *not* set Content-Type headers to allow the browser to set the boundary.
// See https://stanko.github.io/uploading-files-using-fetch-multipart-form-data/ for further explanation.
headers: {'Content-Type': null}
});
}
Here is what I am trying to build:
I have a form with various fields, one of them is image upload. I took the image upload and base64 encode part from here and it works perfectly. This all happens on client-side:
document.getElementById('button').addEventListener('click', function() {
var files = document.getElementById('file').files;
if (files.length > 0) {
getBase64(files[0]);
}
});
function getBase64(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
};
reader.onerror = function (error) {
};
}
Then, I would like to take the base64 encoded string and include it in JSON to make an api call. The api call needs to happen on server-side. It also includes authentication, and this bit alone is also working fine. The call creates a file in the selected location.
The bit that I am struggling with:
How do I pass the fileEncoded variable from the client-side part of the script to the server-side?
I tried the following:
Passing it as query string to a separate page for making the api call: <form action="apicall.html?fileEncoded" method="post"> but I couldn't make this work
Using localStorage, but that didn't work either: localStorage.setItem("fileEncoded", fileEncoded);
I would like to understand what I am missing here and I would appreciate an answer that would explain the concept, on top of providing a code snippet.
I would prefer to do it on two separate pages (1st with form and base64 encode, 2nd with the server-side api call).
You likely need to send a request to your server via POST request, with the encoded file data as the body in that request.
Then server-side, you will have to handle that POST request.
For example:
function getBase64(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
console.log(reader.result);
var fileEncoded = reader.result;
fetch("/upload/file", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: fileEncoded,
// Additional fields here...
})
})
.then(function(res) {
console.log("Success!", res);
})
.catch(function(err) {
console.log("Error!", err);
});
};
reader.onerror = function (error) {
console.log('Error: ', error);
};
}
JS Bin
In your code you are using readAsDataURL which converts the file into a data URL which is typically used to display a selected image file. It doesn't access the server at all. What you want to do is create an AJAX request to the server.
See related question: How to use FormData for ajax file upload
I have a post fetch request coming from my React client to my remote Flask server like so:
fetch(FETCH_URL, {
method: 'POST',
body: data,
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {
var a = response.body.getReader();
a.read().then(({ done, value }) => {
console.log(new TextDecoder("utf-8").decode(value));
}
);
});
response.body comes in the form of a ReadableStream object so I was able to extract a Uint8array which I then decoded to be the contents of the txt file I sent back from my flask server as shown in the code above.
At this point I'm lost, what I'm trying to do is send a request to my remote server with a filename (in the requests' data), and download that file on my computer.
As shown above, I tried a fetch request to my remote server, then in flask, my server finds and opens the file which is stored on the server itself, and sends back the file.
filename = request.get_json()['filename']
f = open(filename)
return f
The problem now is that from what I've read, I can't create a file on my computer just with react. Even so, I don't know if this would work with all types of files or just txt files. Does anyone have any guidance to get to my end goal of downloading a file from a remote flask server.
If your requirement is to create a file with data you received from the response. The below solution should work.
Create the blob object with the text you received
Create Blob Object URL for that blob
Trigger downloading the object using that URL
Since this is pure Javascript solution, it's independent of React or any library you use.
Solution:
fetch(FETCH_URL, {
method: 'POST',
body: data,
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {
var a = response.body.getReader();
a.read().then(({ done, value }) => {
// console.log(new TextDecoder("utf-8").decode(value));
saveAsFile(new TextDecoder("utf-8").decode(value), 'filename');
}
);
});
function saveAsFile(text, filename) {
// Step 1: Create the blob object with the text you received
const type = 'application/text'; // modify or get it from response
const blob = new BlobBuilder([text], {type});
// Step 2: Create Blob Object URL for that blob
const url = URL.createObjectURL(blob);
// Step 3: Trigger downloading the object using that URL
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click(); // triggering it manually
}
Alternatively, you can use <Button href="${YOUR_FILE_URL}"/> to download the file sent by flask.
To add to Kamalakannan's post, if you are never going to use that element again make sure to removeChild() and revokeObjectURL() after triggering the click().
I'm generating HTML webpage as PDF, and then exporting it locally. How can I save this file to my node server and upload to S3
Please find the attached psuedo code
const convertDataToPdf = (exportFlag,cb)=>{ //set to switch between export and save
const doc = new jsPDF();
//... adding metadata and styling the pdf
if(exportFlag) {
doc.save('sample.pdf') //export PDF locally
} else {
cb(doc.output()) //this converts the PDF to raw to send to server
}
}
Based on a this answer, I'm appending the raw PDF data to a new FormData object, and then an ajax call to post the raw data to my nodejs server
convertDataToPdf(false, pdfData => {
let formData = new FormData();
formData.append(`file-1`, pdfData)
$.ajax({
url: '/file-upload',
data: formData,
processData: false,
contentType: false,
type: 'POST',
}).then(data => {
console.log('PDF upload to s3 successful!', data)
}).catch(err => {
console.log('Error! PDF Upload to S3 failed', err)
})
});
});
Now, how can I parse the raw PDF data on the server and upload it?
As an alternative, is it possible to save my file locally and then upload the file to s3?
First question - you can use on Node server multer https://www.npmjs.com/package/multer . This way you don't have to decode pdf. You just handle request and pass file to S3 (via S3 node API). You can use mimetype to be sure someone is sending you pdf.
For sure if you've got application server such as Nginx, you can limit transfer file.
For example in Nginx client_max_body_size 10M;. It's more secure to check limit on server, because naughty users can always cheat your web validations. Multer also has size validation if you would like to return specific exception from your backend.
My users have private files that need to be downloaded by an authenticated users. My server first downloads a file from S3 using it's own S3 app_id/secret_token credentials. The downloaded file is then constructed and sent to the client using Rails' send_data method.
Ruby (on Rails):
# documents_controller.rb
def download
some_file = SomeFile.find(params[:id])
# download file from AWS S3 to server
data = open(some_file.document.url)
# construct and send downloaded file to client
send_data data.read, filename: some_file.document_identifier, disposition: 'inline', stream: 'true'
end
Originally, I wanted to do trigger the download directly from the HTML template.
HTML:
<!-- download-template.html -->
<a target="_self" ng-href="{{ document.download_url }}" download="{{document.file_name}}">Download</a>
Looks simple enough but the problem is that Angular's $http interceptor doesn't catch this type of external link click and therefore the appropriate headers are not appended for server-side authentication. The result is a 401 Unauthorized Error.
Instead, I need to trigger the download using ng-click and then performing an $http.get() request from the angular controller.
HTML:
<!-- download-template.html -->
<div ng-controller="DocumentCtrl">
<a ng-click="download(document)">Download</a>
</div>
Javascript:
// DocumentCtrl.js
module.controller( "DocumentCtrl",
[ "$http", "$scope", "FileSaver", "Blob",
function( $http, $scope, FileSaver, Blob ) {
$scope.download = function( document ) {
$http.get(document.download_url, {}, { responseType: "arraybuffer" } )
.success( function( data ) {
var blob = new Blob([data], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
FileSaver.saveAs(blob, document.file_name);
});
};
}]);
FileSaver is a simple library to save files using Blobs (on the client, obviously).
This gets me passed by my authentication problem but results in the file being saved/download to the client in an unreadable/unusable format.
Why is the file being downloaded in an unusable format?
Thanks in advance.
Angular's $http method needs to be configured to accept a binary data response.
Rails' send_data documentation:
Sends the given binary data to the browser. This method is similar to
render plain: data, but also allows you to specify whether the browser
should display the response as a file attachment (i.e. in a download
dialog) or as inline data. You may also set the content type, the
apparent file name, and other things.
Angular's $http documentation is very poor regarding $http's configuration of responseType. Essentially, $http needs to be told to expect a binary data response by setting responseType to "arraybuffer" (see below).
$scope.download = function( document ) {
console.log("download: ", document);
$http({
url: document.download_url,
method: "GET",
headers: {
"Content-type": "application/json"
},
responseType: "arraybuffer" // expect to handle binary data response
}).success( function( data, status, headers ) {
var type = headers('Content-Type');
var blob = new Blob([data], { type: type });
FileSaver.saveAs(blob, document.file_name);
});
};
Angular's $http documentation could be a little more descriptive than:
Usage
$http(config);
Arguments
config
responseType - {string} - see XMLHttpRequest.responseType.
Hi I have an example of how I download a file from my server with angular:
I call the file with GET request:
file download html(client side):
<a ng-href="/api/downloadFile/{{download.id}}" type="submit" class="btn btn-primary col-lg-12 btn-modal-costume" >download</a>
file download java(server side):
public static Result download(String id) {
String content = null;
for (controllers.file file : files) {
if (file.getId().equals(id)){
content = file.getContent();
}
}
return ok(new java.io.File("/temp/" + id+ "file" + content)).as("application/force-download");
}
If you like you can see the all code in my github project
I think you were on the right track with the javascript solution, but just had a typo. In the $http.get call you pass an empty object as the second parameter. This is where the options argument with {responseType: arraybuffer} should have gone. See docs for $http.get here:
https://docs.angularjs.org/api/ng/service/$http#get