Stream response to download CSV file - php

I am trying to stream a large CSV from the server. I load a JSON file, convert it to an array, and then process it as a CSV. From the frontend, I am calling the following on a button click
downloadCSVData() {
axios({
url: '/downloadCSVData',
method: 'GET'
}).then((response) => {
console.log(response)
});
}
And then this function does the following
public function downloadCSVData()
{
//load JSON file
$data = file_get_contents($file_path, true);
//Convert file to array
$array = json_decode($data, true);
$headers = [
'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0'
, 'Content-type' => 'text/csv'
, 'Content-Disposition' => 'attachment; filename=galleries.csv'
, 'Expires' => '0'
, 'Pragma' => 'public'
];
$response = new StreamedResponse(function() use($array){
// Open output stream
$handle = fopen('php://output', 'w');
// Add CSV headers
fputcsv($handle, array_keys($array['element'][0]), ',');
foreach ($array['element'] as $key => $row) {
fputcsv($handle, $row);
}
// Close the output stream
fclose($handle);
}, 200, $headers);
return $response->send();
}
Now on the frontend, within the console, I can see the reponse being printed the way it needs to be. From my understanding however, the backend should trigger the file to be downloaded which is not happening.
Am I missing something here, how can I get this downloaded on the frontend as a physical file?
Thanks

If you visited the URL directly, your PHP settings should work to force a download. But you are already in a fully loaded HTML page, so you need to use the <iframe> trick to get the file to download. This will allow the browser to receive the incoming data as if you've opened a new browser window.
Try changing your download function to this:
downloadCSVData() {
$('<iframe />')
.attr('src', '/downloadCSVData')
.hide()
.appendTo('body');
}

Related

How to stream binary data from file to POST body in PHP

I'm not a PHP expert so please excuse the lack of correct terminology. Here is some background: There is a service running somewhere on the web and I have to communicate with it via REST API. The API requires me to send binary data (more specifically a ZIP) via POST request. The response will return other binary data that is to be saved in a file.
Since the request data and the response data may be arbitrarily large, I want to stream or buffer the data while reading it from and saving it in a file. The request has to be done in PHP.
Below is a code snippet I use to perform the POST request and handle the response (nevermind missing error handling). As you can see I have two files: "request-data" is a file containing the data I like to POST, "response-data" is the file where I write the response.
$binaryRequestFile = fopen('request-data', 'rb');
$binaryResponseFile = fopen('response-data', 'wb');
$url = "http://some/url";
$headerStr .= "Content-Type: application/zip\r\n";
$options = array(
'http' => array(
'header' => $headerStr,
'follow_location' => false,
'max_redirects' => 0,
'method' => 'POST',
'content' => stream_get_contents($binaryRequestFile),
'ignore_errors' => true
),
);
$context = stream_context_create($options);
$responseData = fopen($url, false, false, $context);
while (!feof($responseData)) {
if (fwrite($binaryResponseFile, fread($responseData, 4 * 1024)) === FALSE) {
return null;
}
}
fclose($responseData);
The code above works, but I'd rather not use stream_get_contents to read the whole request data into memory. Isn't there a way to stream the data directly into the request similar to what is done with the response? I prefer using built-in PHP functionality, no external modules or CURL.

How do you conditionally export a CSV using ajax in laravel? [duplicate]

This question already has answers here:
download file using an ajax request
(13 answers)
Closed 3 years ago.
I'm trying to export a CSV using the Laravel Excel Package https://docs.laravel-excel.com.
I need to pass a few variables in using an ajax request which is all working but the response isn't downloading the CSV.
I'm using the package in a few places without ajax just a simple export and it's all working so I know that side is all good.
Here is the ajax I'm using to send the data:
$(document).ready(function() {
$( "#export" ).on( "click", function() {
let clubInfo = $("#clubInfo select").val();
let funderInfo = $("#funderInfo select").val();
$.ajax({
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
type: 'post',
url: '{{route('export')}}',
data: {club : clubInfo, funder: funderInfo},
}).done(function(response) {
console.log(response);
}).fail(function() {
console.log("Failed");
});
});
});
Here is the controller that handle the export:
public function adminExport(Request $request)
{
$club = $request->input('club');
$funder = $request->input('funder');
return Excel::download(new UsersExport($club, $funder), 'users.xlsx');
}
I'm not getting any PHP errors or warnings which is all good I'm just getting a response in the network tab which looks like it's the csv it's just not downloading.
The headers of the response are:
Accept-Ranges: none
Cache-Control: public
Connection: keep-alive
Content-Disposition: attachment; filename=users.xlsx
Content-Length: 6812
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Unless fetching the file through AJAX is a super hard requirement, I would suggest using window.open('your_url/your_route?club=XXXXX&funder=XXXXX', '_blank'); to download the file and avoiding any headaches.
I think you need to inform the type of export. Note the following:
return (new InvoicesExport)->download('invoices.csv', \Maatwebsite\Excel\Excel::CSV);
But in your controller, I can't see the export type:
return Excel::download(new UsersExport($club, $funder), 'users.xlsx');
If you are using version 2.1, you need to do this:
Excel::create('Filename', function($excel) {
//
})->export('csv');
Or ... what do you think about saving the export first and then exporting it like this?
$file_name = 'my_export';
// Find users first
$users = User::where([['club', $club], ['funder', $funder]])->get();
// then generate the file
Excel::create($file_name, function ($excel) use ($users) {
$excel->sheet('Users', function($sheet) use($users) {
$sheet->fromArray($users);
});
})->store('csv', storage_path('excel/export'));
// Finally, download the file
$full_path = storage_path('excel/export') . '/' . $file_name . '.csv';
return Storage::disk('public')->download($full_path, $file_name . 'csv',
['Content-Description' => 'File Transfer','Content-Type' => 'application/octet-
stream','Content-Disposition' => 'attachment; filename=' . $file_name .
'.csv']);
or maybe you would do that:
$headers = array(
'Content-Type' => 'text/csv',
);
return Response::download($file_name . 'csv', $full_path, $headers);

Angular 4 download xls [duplicate]

This question already has answers here:
How to download excel/Zip files in Angular 4
(2 answers)
Closed 5 years ago.
i'm trying to download excel file (.xls) using angular and php for backend
my backend already send my excel file as response, and when i check it, it return with correct format
but my angular 4 is returning the file as corrupted format (it contain some symbols like ��ࡱ�;�� )
below is my angular code:
Service
private headers = new Headers({
'Content-Type': 'application/x-www-form-urlencoded'
});
downloadTemplate() {
this.options = new RequestOptions();
this.options.headers = this.headers;
const params: URLSearchParams = new URLSearchParams();
params.set('token', this.currentUser.token);
this.options.search = params;
return this.http.get(environment.api_host + '/template', this.options);
}
Component
template() {
this._apiService.downloadTemplate().subscribe((response) => {
const blob = new Blob([(<any>response)._body], {type: 'application/vnd.ms-excel'});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'template.xls';
document.body.appendChild(link);
link.click();
});
}
from my code, is there something that i miss?
If subscription is not necessary then just use this in downloadTemplate() method in your service
window.location.href = your_URL;
I'm using FileSaver library for serving files from Angular apps: http://alferov.github.io/angular-file-saver/
Here you have sample code:
getTemplate(): Observable<Blob> {
this.apiService.http()
.get(environment.api_host + '/template', { responseType: ResponseContentType.Blob })
.map(r => r.blob())
.subscribe(
template => {
FileSaver.saveAs(new Blob([template]), "template.xml");
},
error => {
console.error(error);
}
);
}
Important is that you should add header Content-Type: application/octet-stream to your PHP response. Complete and working set of headers:
$file = fopen("/tmp/examplefile.xls", "r");
array(
'Content-Disposition' => 'attachment; filename="' . basename($filePath) . '"',
'Content-Type' => 'application/octet-stream',
'Content-Length' => filesize($filePath),
'Expires' => '#0',
'Cache-Control' => 'must-revalidate',
'Pragma' => 'public'
);
I think the problem in your code is header definition: const blob = new Blob([(<any>response)._body], {type: 'application/vnd.ms-excel'}); or response type in http client options. You should send 'octet/stream' from PHP(binary data). In angular create Blob object from this data and return Blob object to user. That's all.

Can't direct download after creating a CSV file

I've seen a lot of answers of this type of questions but none worked for me. I've a CSV object which has a createCSV function like this :
public function createCSV($url, $delimiter = ","){
$file = fopen($url, 'w');
foreach ($this->content as $line) {
fputcsv($file, $line, $delimiter);
}
fclose($file);
}
And I want to download it directly from the browser so here is what I do :
header('Content-type: text/csv');
header('Content-Disposition: attachment; filename="'.$filename.'"');
$csv_file->createCSV('php://output');
This part of code is execute with an AJAX call and even if the header is set to text/csv the download doesn't work but in the response tab I can see the content of my csv. I've tried with different header but none of them worked. How can I do to download the CSV ?
EDIT
The thing is that I don't have a URL for my CSV and I don't want to store the file somewhere, I just want to build the file and download directly with the browser
Try this code:
$('#exportcsv').click(function(){
var self = this;
$.ajax({
url : '/exportcsv',
method : 'get',
success : function(response)
{
console.log(response);
var csvData = 'data:application/csv;charset=UTF-8,' + encodeURIComponent(response);
$(self).attr({
'download': 'publisher.csv',
'href': csvData,
'target': '_blank'
});
// window.open(uri, 'test.csv');
}
})
})

How to use ajax to execute php function that will push a file to the browser?

I'm trying to write a method in a php class that will use ajax to execute a php function that will push a file back to the browser.
It seems like its trying to write the file to the modx log, getting a lot of binary garbage in there.
Here is the method:
public function pushDocuments($formdata){
$data = $formdata['formdata'];
$file = MODX_PROTECTED_STORAGE . $data['target'];
$file_name = basename($file);
if (file_exists($file)) {
header("Content-Disposition: attachment; filename=\"$file_name\"");
header("Content-Length: " . filesize($file));
header("Content-Type: application/octet-stream;");
readfile($file);
};
$output = array(
'status' => 'success',
'error_messages' => array(),
'success_messages' => array(),
);
$output = $this->modx->toJSON($output);
return $output;
}
and here is the jquery:
$('.btn-get-document').click(function(){
var target = $(this).attr('data-target');
var postdata = {"snippet":"DataSync", "function":"pushDocuments", "target": target}; // data object ~ not json!!
console.log('target = ' + target + postdata );
$.ajax({
type: "POST",
url: "processors/processor.ajax.generic/",
dataType : "json",
cache : false,
data: postdata, // posting object, not json
success: function(data){
if(data.status == 'success'){
console.log("SUCCESS status posting data");
}else if(data.status == 'error'){
console.log("error status posting data");
}
},
error: function(data){
console.log("FATAL: error posting data");
}
});
});
it's running through the scripts and giving a success in the console [because I am forcing success] but no file is prompted for download and the binary garbage shows up in the modx log
What am I doing wrong?
In order to download a file, you'd have to use JS to redirect to the file's location. You can't pull the file contents through AJAX and direct the browser to save those contents as a file.
You would need to structurally change your setup. For instance, your PHP script can verify the existence of the file to be downloaded, then send a link to JS in order to download the file. Something like this:
if ( file_exists( $file )) {
$success_message = array(
'file_url' => 'http://example.com/file/to/download.zip'
);
}
$output = array(
'status' => 'success',
'error_messages' => array(),
'success_messages' => $success_message
);
Then modify the "success" portion of your AJAX return like this:
success: function( data ) {
if ( data.status == 'success' ) {
location.href = data.success_messages.file_url;
} else if ( data.status == 'error' ) {
console.log( 'error status posting data' );
}
},
Since you're directing to a file, the browser window won't actually go anywhere, so long as the file's content-disposition is set to attachment. Typically this would happen if you directed to any file the browser didn't internally handle (like a ZIP file). If you want control over this so that it downloads all files (including things the browser may handle with plugins), you can direct to another PHP script that would send the appropriate headers and then send the file (similar to the way you're sending the headers and using readfile() in your example).
#sean-kimball,
You might want to extend MODX's class based processor instead:
https://github.com/modxcms/revolution/blob/master/core/model/modx/processors/browser/file/download.class.php
It does the download from any media source and also access checking if you want.
Its implementation on manager side is:
https://github.com/modxcms/revolution/blob/master/manager/assets/modext/widgets/system/modx.tree.directory.js#L553
Back to your case, these examples might bring you some ideas.
JS Example:
$.ajax({
type: "POST",
// read my note down below about connector file
url: "assets/components/mypackage/connectors/web.php",
dataType : "json",
cache : false,
data: {
action: 'mypath/to/processor/classfile'
}
success: function(data){
},
error: function(data){
console.log("FATAL: error posting data");
}
});
Processor example:
<?php
require_once MODX_CORE_PATH . 'model/modx/processors/browser/file/download.class.php';
class myDownloadProcessor extends modBrowserFileDownloadProcessor {
// override things in here
}
return 'myDownloadProcessor';
For this, I also suggest you to use MODX's index.php main file as the AJAX's connector so the $modx object in processor inherits the access permission as well.
http://www.virtudraft.com/blog/ajaxs-connector-file-using-modxs-main-index.php.html

Categories