THE SITUATION:
Frontend: Vue. Backend: Laravel.
Inside the web app I need to let the user download certain pdf files:
I need Laravel to take the file and return it as a response of an API GET request.
Then inside my Vue web app I need to get the file and download it.
THE CODE:
API:
$file = public_path() . "/path/test.pdf";
$headers = [
'Content-Type' => 'application/pdf',
];
return response()->download($file, 'test.pdf', $headers);
Web app:
downloadFile() {
this.$http.get(this.apiPath + '/download_pdf')
.then(response => {
let blob = new Blob([response.data], { type: 'application/pdf' })
let link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'test.pdf'
link.click()
})
}
OUTCOME:
Using this code I do manage to download a pdf file. The problem is that the pdf is blank.
Somehow the data got corrupted (not a problem of this particular pdf file, I have tried with several pdf files - same outcome)
RESPONSE FROM SERVER:
The response itself from the server is fine:
PDF:
The problem may be with the pdf file. It definitely looks corrupted data. This is an excerpt of how it looks like the response.data:
THE QUESTION:
How can I properly download a pdf file using Laravel for the API and Vue for the web app?
Thanks!
SOLUTION:
The code above was correct. What was missing was adding the proper responseType as arraybuffer.
I got scared by those ???? inside the response, and that was misleading me.
Those question marks were just okay since pdf is a binary data and is meant to be read by a proper reader.
THE ARRAYBUFFER:
And arraybuffer is precisely used to keep binary data.
This is the definition from the mozilla website:
The ArrayBuffer object is used to represent a generic, fixed-length
raw binary data buffer. You cannot directly manipulate the contents of
an ArrayBuffer; instead, you create one of the typed array objects or
a DataView object which represents the buffer in a specific format,
and use that to read and write the contents of the buffer.
And the ResponseType string indicates the type of the response. By telling its an arraybuffer, it then treats the data accordingly.
And just by adding the responseType I managed to properly download the pdf file.
THE CODE:
This is corrected Vue code (exactly as before, but with the addition of the responseType):
downloadFile() {
this.$http.get(this.appApiPath + '/testpdf', {responseType: 'arraybuffer'})
.then(response => {
let blob = new Blob([response.data], { type: 'application/pdf' })
let link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'test.pdf'
link.click()
})
}
EDIT:
This is a more complete solution that take into account other browsers behavior:
downloadContract(booking) {
this.$http.get(this.appApiPath + '/download_contract/' + booking.id, {responseType: 'arraybuffer'})
.then(response => {
this.downloadFile(response, 'customFilename')
}, response => {
console.warn('error from download_contract')
console.log(response)
// Manage errors
}
})
},
downloadFile(response, filename) {
// It is necessary to create a new blob object with mime-type explicitly set
// otherwise only Chrome works like it should
var newBlob = new Blob([response.body], {type: 'application/pdf'})
// IE doesn't allow using a blob object directly as link href
// instead it is necessary to use msSaveOrOpenBlob
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(newBlob)
return
}
// For other browsers:
// Create a link pointing to the ObjectURL containing the blob.
const data = window.URL.createObjectURL(newBlob)
var link = document.createElement('a')
link.href = data
link.download = filename + '.pdf'
link.click()
setTimeout(function () {
// For Firefox it is necessary to delay revoking the ObjectURL
window.URL.revokeObjectURL(data)
}, 100)
},
You won't be able to do the download from Laravel to Vue since both are running at different ports I assume.
Even if you try something like this.
public function getDownload()
{
//PDF file is stored under project/public/download/info.pdf
$file= public_path(). "/download/info.pdf";
$headers = [
'Content-Type' => 'application/pdf',
];
return response()->download($file, 'filename.pdf', $headers);
}
It won't help as you are sending headers to the Laravel Port Try using Vue js libraries and try to send that pdf content on the library
Try this
Get help from here
it's works for me.
from laravel backend:
$pdf = PDF::loadView('your_view_name', ['data' => $data]);
return $pdf->output();
from vuejs frontend:
axios({
url: 'http://localhost:8000/api/your-route',
method: 'GET',
responseType: 'blob',
}).then((response) => {
var fileURL = window.URL.createObjectURL(new Blob([response.data]));
var fileLink = document.createElement('a');
fileLink.href = fileURL;
fileLink.setAttribute('download', 'file.pdf');
document.body.appendChild(fileLink);
fileLink.click();
});
downloadFile: function () {
this.$http.post('{{ route('download.download') }}', {
_token: "{{ csrf_token() }}",
inputs: this.inputs
},{responseType: 'arraybuffer'}).then(response => {
var filename = response.headers.get('content-disposition').split('=')[1].replace(/^\"+|\"+$/g, '')
var url = window.URL.createObjectURL(new Blob([response.body],{type:response.headers.get('content-type')}))
var link = document.createElement('a')
link.href = url
link.setAttribute('download', filename)
document.body.appendChild(link)
link.click()
});
},
Related
I have written function where I want to download an xlsx file via a service. Download also works so far. But when I open the file I get the error message file extension or file format is invalid. How can I solve the problem?
Code:
// Service
getDownloadPlan(): Observable<any> {
const url = `/download-plan?sales-plan=0&personnel-plan=0&investment-plan=0&loan-plan=0&material-cost-plan=0`;
return this.http.get(`${environment.baseUrl}` + url, { responseType: 'blob'});
}
// TS
downloadPlanBwa() {
this.planBwaService.getDownloadPlan().subscribe(response => {
const downloadFile: any = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
fileSaver.saveAs(downloadFile, 'Plan');
}, error => console.log('ERROR'),
() => console.log('SUCCESSFUL')
);
}
If i use the MIME-Type application/vnd.ms-excel;charset=utf-8 this is for the xls-format then it works.
What do I need to change in my code to successfully open xlsx files?
I did as in the documentation (https://developers.google.com/drive/api/v3/manage-uploads#http---single-request), but it doesn't work:
var fileMetadata = {
name: e.target.files[j].name,
parents: this.currentDirectoryId ? [this.currentDirectoryId] : []
}
var media = {
mimeType: e.target.files[j].type,
body: e.target.files[j]
}
window.gapi.client.drive.files.create({
resource: fileMetadata,
media: media,
fields: 'id, name, mimeType, createdTime'
}).then(res => console.log(res))
File is created, but empty and named "Untitled" with mimeType "application/octet-stream"
Issue and workaround:
When I tested gapi.client.drive.files.create, it seems that although this method can create new file with the metadata, the file content cannot be included. So in this answer, in order to upload a file by including the file metadata, I would like to propose to upload a file with multipart/form-data using fetch of Javascript. In this case, the access token is retrieved by gapi.auth.getToken().access_token.
Unfortunately, from your script, I couldn't understand about e.target. So in this sample script, I would like to propose the sample script for uploading a file, which is retrieved from the input tag, with the metadata.
Sample script:
HTML side:
<input type="file" id="files" name="file">
Javascript side:
const files = document.getElementById("files").files;
const file = files[0];
const fr = new FileReader();
fr.readAsArrayBuffer(file);
fr.onload = (f) => {
const fileMetadata = {
name: file.name,
parents: this.currentDirectoryId ? [this.currentDirectoryId] : [] // This is from your script.
}
const form = new FormData();
form.append('metadata', new Blob([JSON.stringify(fileMetadata)], {type: 'application/json'}));
form.append('file', new Blob([new Uint8Array(f.target.result)], {type: file.type}));
fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', {
method: 'POST',
headers: new Headers({'Authorization': 'Bearer ' + gapi.auth.getToken().access_token}),
body: form
}).then(res => res.json()).then(res => console.log(res));
};
In this script, the file retrieved from input tag is uploaded to Google Drive with multipart/form-data.
Note:
In this script, it supposes that your authorization script can be used for uploading a file to Google Drive. Please be careful this.
In this answer, as a sample script, the file is uploaded with uploadType=multipart. In this case, the maximum file size is 5 MB. Please be careful this. When you want to upload the file with the large size, please check the resumable upload. Ref
References:
Using Fetch
Files: create
Upload file data
Perform a resumable upload
I have problems with exporting tables in javascript. Backend is spring and my method in controller look like this.
#PostMapping(produces = "application/vnd.ms-excel")
public void report(#RequestBody #Validated final ReportRequest reportRequest, final HttpServletResponse response, final Principal principal) {
log.info("'{}' Requested report '{}'", principal.getName(), reportRequest);
final List<Data> dataList = dataRepository.findAll(
findByCriteria(
reportRequest.getFilterDatas(),
reportRequest.getId(),
reportRequest.getStartDate(),
reportRequest.getEndDate()));
final SXSSFWorkbook workbook = excelService.generateExcelFromDraData(dataList, FILE_NAME);
writeToOutputStream(response, workbook);
}
On frontend I use vue.js and axios for http client. And export method is:
axios.post(
url+'report',
query,
{headers: {
"Access-Control-Allow-Headers" : "*",
"X-XSRF-TOKEN": this.$cookie.get('XSRF-TOKEN')
}
}
)
.then((response) => {
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
var blob = new Blob([response.data], {type: "application/vnd.ms-excel"});
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = 'report.xlsx';
a.click();
window.URL.revokeObjectURL(url);
}, (error) => {
}
)
When I hit 'Send and Download' with postman, I get excel that i want. But when i do that from client, I get bytes in response in console.log, but I'cant open excel with message 'excel cannot open the file because the file format or file extension is not valid...'. If i put report.xls for name, I get excel i can open, but with some bytes that mean nothing.
Any suggestion what is wrong?
.xlsx has different MIME type:
.xlsx: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Note that also browsers handle file downloading in different ways. I've used successfully following code (you'll have to change it a bit to use in your app):
function successCallback (data) { //In my case data was already a Blob
if (window.navigator.msSaveOrOpenBlob) { //for IE
window.navigator.msSaveOrOpenBlob(data, 'file.xlsx');
} else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(data.slice());
a.target = '_blank';
a.download = 'file.xlsx';
a.dataset.downloadurl = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', a.download, a.href].join(':');
//a.click() got cancelled in firefox
var event = document.createEvent("MouseEvent");
event.initMouseEvent(
"click",
true /* bubble */,
false /* cancelable */,
window, null,
0, 0, 0, 0, /* coordinates */
false, false, false, false, /* modifier keys */
0 /*left*/, null
);
a.dispatchEvent(event);
}
}
A web page (front) is calling a service which send a PDF stream as a response :
Here is the front code :
'click .btn': function (event) {
/.../
event.preventDefault();
Http.call(params, (err, res) => { // callback
if (err) console.log(err); // nothing
console.log({ res }); // print below result
const blob = new Blob(
[res.content],
{ type: `${res.headers['content-type']};base64` }
);
saveAs(blob, res.headers['content-disposition'].slice(21));
});
}
Here is the response from the server ( console.log(res) ) : { res : Object } printed in the console.
content: "%PDF-1.4↵1 0 obj↵<<↵/Title (��)↵/Creator (��)↵/Prod ..... lot of characters....%"
data: null,
statusCode: 200,
headers: {
connection: "close",
content-disposition: "attachment; filename=myDoc.pdf"
content-type: "application/pdf",
date: "date",
transfer-encoding: "chunked",
x-powered-by: "Express"
}
However, the PDF is downloaded with no content, it's full blank like corrupted ( But I can see the content in the string ). It works well with the CSV routes ( I send a csv as a stream and download it with the same method and I got the data).
I think there is something with the format %PDF ...% but I didn't manage to find something.
Note : With postman, it works, my PDF is saved, the page is not blank, I got the data. So there is something in the front I am not doing right.
I also tried with :
const fileURL = URL.createObjectURL(blob);
window.open(fileURL); // instead of saveAs
but the result is the same ( but in another tab instead of saved PDF ) blank page.
Any ideas ?
You probably forgot to specify the response type in your inital backend call - from the example you posted "arraybuffer" would be the correct one here, you can check all types here.
I am trying to upload a sound file from ngCordova's $cordovaCapture service to UploadCare. The uploadcare.fileFrom('object') keeps failing with an'upload' error. I have the public key set. I am able to upload the file by sending it through and tag and accessing document.getElementById('fileTag').files[0].
$cordovaCapture.captureAudio()
.then(function (audioData) {
return uploadcare.fileFrom('object', audioData[0])
.done(function (fileInfo) {
console.log(fileInfo);
}).fail(function (err) {
console.log(err);
})
})
the audioData[0] object looks like this
MediaFile {
end:0
fullPath:"file:/storage/emulated/0/Sounds/Voice%20002.m4a"
lastModified:null
lastModifiedDate:1481324751000
localURL:"cdvfile://localhost/sdcard/Sounds/Voice%20002.m4a"
name:"Voice 002.m4a"
size:49227
start:0
type:"audio/mpeg"
} __proto__:File
I thought the problem might be that the object is a MediaFile rather than a File but I could use some help casting one to the other.
FileEntry
filesystem:FileSystem
fullPath:"/Sounds/Voice 002.m4a"
isDirectory:false
isFile:true
name:"Voice 002.m4a"
nativeURL:"file:///storage/emulated/0/Sounds/Voice%20002.m4a"
__proto__:Entry
File
end:49227
lastModified:1481324751000
lastModifiedDate:1481324751000
localURL:"cdvfile://localhost/sdcard/Sounds/Voice%20002.m4a"
name:"Voice 002.m4a"
size:49227
start:0
type:"audio/mpeg"
__proto__:Object
using window.resolveLocalFileSystemUrl() you end up with the above FileEntry object that give the above File object but uploadcare still fails with an "upload" error.
Using ngCordova $cordovaFileTransfer() you can send audio files to uploadcare.
var fileName = filePath.split('/').pop();
var uploadcareOptions = {
fileKey: "file",
fileName: fileName,
chunkedMode: false,
mimeType: 'audio/mp4',
params: {
"UPLOADCARE_PUB_KEY": "upload-care-public-key",
"UPLOADCARE_STORE": 'auto',
fileName: fileName
}
};
return $cordovaFileTransfer.upload('https://upload.uploadcare.com/base/', filePath, uploadcareOptions)
The important part is to specify the mime type when sending files as uploadcare will assume it's a image otherwise.
uploadcare.fileFrom uploads a file from a native file object. Try this:
window.resolveLocalFileSystemURL(audioData[0].localURL,function(fileEntry){
fileEntry.file(function(file) {
uploadcare.fileFrom('object', file);
...
});
});