I have a script that generates a large CSV file using fputcsv and sends it to the browser. It works, but the browser doesn't show the file download prompt (or start downloading the file) until the whole CSV file has been generated serverside, which takes a long time.
Instead, I'd like the download to begin while the remainder of the file has still being generated. I know this is possible because it's how the 'Export database' option in PHPMyAdmin works - the download starts as soon as you click the 'export' button even if your database is huge.
How can I tweak my existing code, below, to let the download begin immediately?
$csv = 'title.csv';
header( "Content-Type: text/csv;charset=utf-8" );
header( "Content-Disposition: attachment;filename=\"$csv\"" );
header( "Pragma: no-cache" );
header( "Expires: 0" );
$fp = fopen('php://output', 'w');
fputcsv($fp, array_keys($array), ';', '"');
foreach ($array as $fields)
{
fputcsv($fp, $fields, ';', '"');
}
fclose($fp);
exit();
Empirically, it seems that when receiving responses featuring a Content-Disposition: attachment header, different browsers will show the file download dialog at the following moments:
Firefox shows the dialog as soon as it receives the headers
Internet Explorer shows the dialog once it has received the headers plus 255 bytes of the response body.
Chromium shows the dialog once it has received the headers plus 1023 bytes of the response body.
Our objectives, then, are as follows:
Flush the first kilobyte of the response body to the browser as soon as possible, so that Chrome users see the file download dialog at the earliest possible moment.
Thereafter, regularly send more content to the browser.
Standing in the way of these objectives are, potentially, multiple levels of buffering, which you can try to fight in different ways.
PHP's output_buffer
If you have output_buffering set to a value other than Off, PHP will automatically create an output buffer which stores all output your script tries to send to the response body. You can prevent this by ensuring that you have output_buffering set to Off from your php.ini file, or from a webserver config file like apache.conf or nginx.conf. Alternatively, you can turn off the output buffer, if one exists, at the start of your script using ob_end_flush() or ob_end_clean():
if (ob_get_level()) {
ob_end_clean();
}
Buffering done by your webserver
Once your output gets past the PHP output buffer, it may be buffered by your webserver. You can try to get around this by calling flush() regularly (e.g. every 100 lines), although the PHP manual is hesitant about providing any guarantees, listing some particular cases where this may fail:
flush
...
Flushes the write buffers of PHP and whatever backend PHP is using (CGI, a web server, etc). This attempts to push current output all the way to the browser with a few caveats.
flush() may not be able to override the buffering scheme of your web server ...
Several servers, especially on Win32, will still buffer the output from your script until it terminates before transmitting the results to the browser.
Server modules for Apache like mod_gzip may do buffering of their own that will cause flush() to not result in data being sent immediately to the client.
You can alternatively have PHP call flush() automatically every time you try to echo any output, by calling ob_implicit_flush at the start of your script - though beware that if you have gzip enabled via a mechanism that respects flush() calls, such as Apache's mod_deflate module, this regular flushing will cripple its compression attempts and probably result in your 'compressed' output being larger than if it were uncompressed. Explicitly calling flush() every n lines of output, for some modest but non-tiny n, is thus perhaps a better practice.
Putting it all together, then, you should probably tweak your script to look something like this:
<?php
if (ob_get_level()) {
ob_end_clean();
}
$csv = 'title.csv';
header( "Content-Type: text/csv;charset=utf-8" );
header( "Content-Disposition: attachment;filename=\"$csv\"" );
header( "Pragma: no-cache" );
header( "Expires: 0" );
flush(); // Get the headers out immediately to show the download dialog
// in Firefox
$array = get_your_csv_data(); // This needs to be fast, of course
$fp = fopen('php://output', 'w');
fputcsv($fp, array_keys($array), ';', '"');
foreach ($array as $i => $fields)
{
fputcsv($fp, $fields, ';', '"');
if ($i % 100 == 0) {
flush(); // Attempt to flush output to the browser every 100 lines.
// You may want to tweak this number based upon the size of
// your CSV rows.
}
}
fclose($fp);
?>
If this doesn't work, then I don't think there's anything more you can do from your PHP code to try to resolve the problem - you need to figure out what's causing your web server to buffer your output and try to solve that using your server's configuration files.
have not tested this. try to flush the script after n number of data rows.
flush();
Try Mark Amery's answer, but just emphasize on the statement:
$array = get_your_csv_data(); // This needs to be fast, of course
If you're fetching huge number of records, fetch them by chunks (every 1000 records for example).
So:
Fetch 1000 records
Output them
Repeat
I think you are looking for the octet-stream header.
$csv = 'title.csv';
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment;filename=\"$csv\"" );
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Expires: 0');
$fp = fopen('php://output', 'w');
fputcsv($fp, array_keys($array), ';', '"');
foreach ($array as $fields)
{
fputcsv($fp, $fields, ';', '"');
}
fclose($fp);
exit();
Related
Im trying to make a CSV export from data entered in an array on my website. I was using this question to help me. I am getting the data that should be in the CSV echoed on my website but not exported to a file. This is the code that I took from the question:
header( "Content-Type: text/csv;charset=utf-8" );
header( "Content-Disposition: attachment;filename=\"$filename\"" );
header("Pragma: no-cache");
header("Expires: 0");
$fp= fopen('php://output', 'w');
foreach ($data as $fields){
fputcsv($fp, $fields);
}
fclose($fp);
exit();
I dont exactly understand what the header() functions are doing. How would I get this to download to a file?
if it helps my array is in this format:
$data = array(dataset1(array, of, data), dataset2(array, of, data), dataset#(array, of, data));
EDIT:My $data array is in a session varible and the reason it wasnt downloading was because there I had session_start() and some includes at the top. Instead of downloading it would echo to the screen but if I remove this it downloads at the cost of there being no data to export. Anyone have a solution to this?
The header() function is sending HTTP headers to your browser with the respective values.
It then sends the CSV data to the output stream which the browser interprets as a downloadable file due to the headers.
I'm trying to pass a large file from an external API to the user, (think 100MB or more)
Currently, I'm using a bit of a paranoid script (due to failures from the past) to get the script downloading ASAP.
By 'downloading', I only mean the download trigger on the browser, not the actual downloading of the file. Just the point where user can select where (s)he wants to save the file.
set_time_limit(0);
apache_setenv('no-gzip', 1);
ini_set('zlib.output_compression', 0);
ini_set('output_buffering', 0);
ini_set('implicit_flush', 1);
for($i = 0; $i < ob_get_level(); $i++) { ob_end_flush(); }
ob_implicit_flush(1);
header('Content-Description: File Transfer');
header('Content-type: application/octet-stream');
header('Content-Transfer-Encoding: Binary');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: private');
ob_flush();
flush();
$fh = fopen($external_api_url, 'rb');
while(!feof($fh))
{
echo fread($fh, 512);
ob_flush();
flush();
}
fclose($fh);
Using this script, it still takes 20 seconds for a 50mb file before the download popup shows up, and much longer with bigger files.
Is there any way to start the stream faster?
EDIT:
I've also tried fpassthru() and readfile() but these take 40 seconds for the same 50mb file, making me think this way is better. I've also played around with different read sizes (512, 256, 64, couple of others) but I didn't notice a difference)
The reason it is taking so long for your browser to trigger the download dialog, is due to the API taking that long to return its first chunk of data, for example they might be reading the whole file to memory before starting to send the data to you, which would explain why longer files take longer to start even though you always try to write the first 512 bytes as quickly as possible.
I tried to simulate it locally by reading from a local file, but having a sleep(5); statement right before your while loop.
I was able to get Google Chrome to start the download before any data, by omitting the header('Content-type: application/octet-stream'); line, while still issuing a flush(); call before attempting to read the file. (You are already doing this second part)
This however doesn't seem to work with Firefox 3.6, so you might need different gimmicks for different browsers, unless you can predict the first character of a file (Take a look at BOM) and echo that before anything, subsequently removing it from the beginning of your first fread() call.
I hope it helps! But basically the external API is screwing you over.
I need to write a CSV file to the PHP output buffer and then download that file to the client's computer after it's done writing. (I wanted to just write it on the server and download it which was working, but it turns out I won't have write access on production servers).
I have the following PHP script:
$basic_info = fopen("php://output", 'w');
$basic_header = array(HEADER_ITEMS_IN_HERE);
#fputcsv($basic_info, $basic_header);
while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {
#fputcsv($basic_info, $user_row);
}
#fclose($basic_info);
header('Content-Description: File Transfer');
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=test.csv');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize("php://output"));
ob_clean();
flush();
readfile("php://output");
I'm not sure what to do. The CSV file downloads but displays nothing. I assume it has something to do with the ordering of my ob_clean() and flush() commands, but I'm not sure what's the best way to order these things.
Any help is appreciated.
You're doing a little too much. Create the script with the sole purpose of outputting the CSV. Just print it out directly to the screen. Don't worry about headers or buffers or php://output or anything like that yet.
Once you've confirmed that you're printing the data out to the screen appropriately, just add these headers at the beginning:
<?php
header("Content-disposition: attachment; filename=test.csv");
header("Content-Type: text/csv");
?>
... confirm that that downloads the file appropriately. Then you can add the other headers if you like (the headers I included above are those I've used myself without any extra cruft to get this working, the others are basically for efficiency and cache control, some of which may already be handled appropriately by your server, and may or may not be important for your particular application).
If you want, use output buffering with ob_start() and ob_get_clean() to get the output contents into a string which you can then use to fill out Content-Length.
As mentioned in my comments of Edson's answer, I expected a "headers already sent" warning at the last line of code:
header('Content-Length: '.$streamSize);
since output is written before this header is sent, but his example works ok.
Some investigation leads me to to following conclusions:
At the time you use an output buffer (weither a user one, or the
default PHP one), you may send HTTP headers and content the way you
want. You know that any protocol require to send headers before body
(thus the term "header"), but when you use an ouput buffer layer, PHP
will take care of this for you. Any PHP function playing with output
headers (header(), setcookie(), session_start()) will in fact use the
internal sapi_header_op() function which just fills in the headers
buffer. When you then write output, using say printf(), it writes into
the output buffer (assuming one). When the output buffer is to be
sent, PHP starts sending the headers first, and then the body. PHP
takes care of everything for you. If you dont like this behavior, you
have no other choice than disabling any output buffer layer.
and
The default size of the PHP buffer under most configurations is 4096
bytes (4KB) which means PHP buffers can hold data up to 4KB. Once this
limit is exceeded or PHP code execution is finished, buffered content
is automatically sent to whatever back end PHP is being used (CGI,
mod_php, FastCGI). Output buffering is always Off in PHP-CLI.
Edson's code works because the output buffer did not automatically get flushed because it doesn't exceed the buffer size (and the script isn't terminated obviously before the last header is sent).
As soon as the data in the output buffer exceeds the buffer size, the warning will be raised. Or in his example, when the data of
$get_users_stmt->fetch(PDO::FETCH_ASSOC)
is too large.
To prevent this, you should manage the output buffering yourself with the ob_start() and ob_end_flush(); like below:
// Turn on output buffering
ob_start();
// Define handle to output stream
$basic_info = fopen("php://output", 'w');
// Define and write header row to csv output
$basic_header = array('Header1', 'Header2');
fputcsv($basic_info, $basic_header);
$count = 0; // Auxiliary variable to write csv header in a different way
// Get data for remaining rows and write this rows to csv output
while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {
if ($count == 0) {
// Write select column's names as CSV header
fputcsv($basic_info, array_keys($user_row));
} else {
//Write data row
fputcsv($basic_info, $user_row);
}
$count++;
}
// Get size of output after last output data sent
$streamSize = ob_get_length();
//Close the filepointer
fclose($basic_info);
// Send the raw HTTP headers
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=test.csv');
header('Expires: 0');
header('Cache-Control: no-cache');
header('Content-Length: '. ob_get_length());
// Flush (send) the output buffer and turn off output buffering
ob_end_flush();
You're still bound to other limits, though.
I did some tweaks to your code.
Moved headers before any output, as suggested by PHP's doc;
Remember that header() must be called before any actual output is
sent, either by normal HTML tags, blank lines in a file, or from PHP
Removed some headers that didn't make much of a change;
Commented another option of writing csv header using the select column's names;
Now content length works;
There is no need to echo $basic_info as it is already at output buffer and we redirected it inside a file through headers;
Removed # (PHP Error Control Operator) as it may cause overhead, don't have a link to show you right now but you might find it if you search. You should think twice before silencing errors, most times it should be fixed instead of silenced.
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=test.csv');
header('Expires: 0');
header('Cache-Control: no-cache');
$basic_info = fopen("php://output", 'w');
$basic_header = array(HEADER_ITEMS_IN_HERE);
fputcsv($basic_info, $basic_header);
$count = 0; // auxiliary variable to write csv header in a different way
while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {
// Write select column's names as CSV header
if ($count == 0) {
fputcsv($basic_info, array_keys($user_row));
}
fputcsv($basic_info, $user_row);
$count++;
}
// get size of output after last output data sent
$streamSize = ob_get_length();
fclose($basic_info);
header('Content-Length: '.$streamSize);
I'm serving some records from a MySQL database using PHP's fputcsv() by creating a file on the server, filling it, then linking to it on the next page.
This works and is great but as this could be sensitive data, I don't want a buch of files hanging about on the server when they were created for (probably) a one-time download.
So what I want to know is this: is there a way to create this file & serve it for download without actually writing a permanent file on the server?
For instance could I create a comma separated string instead of using fputcsv() and serve that with the right headers in an output buffer?
The obvious move is to delete the file but I need to wait until the client downloads it first so that makes it a little difficult to decide when to do it.
Any suggestions welcome
The code:
$fp = fopen($filename, 'w');
fputcsv($fp, array("Last Name", "First Name"));
foreach ($result as $fields)
{
fputcsv($fp, $fields);
}
fclose($fp);
http://php.net/manual/en/function.fputcsv.php
fputcsv() is a fabulous little function, so I wouldn't abandon it.
Instead, I suggest you play around with PHP's built-in I/O Wrappers
You, can, for example, do this to "stream" your CSV data line-by-line (subject to various output buffers, but that's another story):
<?php
header('Content-type: text/csv; charset=UTF-8');
header('Content-disposition: attachment; filename=report.csv');
$fp = fopen('php://output','w');
foreach($arrays as $array) fputcsv($fp, $array);
That works great, but if something goes wrong, your users will have a broken download.
So, if you don't have too much data, you can just write to an in-memory stream, just swap out php://output with php://memory and move things around:
<?php
$fp = fopen('php://memory','rw');
// our generateData() function might throw an exception, in which case
// we want to fail gracefully, not send the user a broken/incomplete csv.
try {
while($row = generateData()) fputcsv($fp, $row);
}catch(\Exception $e){
// display a nice page to your user and exit/return
}
// SUCCESS! - so now we have CSV data in memory. Almost like we'd spooled it to a file
// on disk, but we didn't touch the disk.
//rewind our file handle
rewind($fp);
//send output
header('Content-type: text/csv; charset=UTF-8');
header('Content-disposition: attachment; filename=report.csv');
stream_get_contents($fp);
Rather than that, why not just have your page echo out a csv mime type and then echo out the file to the user?
It works a charm, the file is never created and passed as a one off to the client.
Something like this:
header("Content-type: application/csv");
header("Content-Disposition: attachment; filename=file.csv");
header("Pragma: no-cache");
header("Expires: 0");
echo "col1,col2";
for($i=0; $i<25;$i++)
{
echo "key :".$i.", ".($i*$i)."\r\n";
}
You should be able to test that out as is and see how it works.
The added beauty is that most users will be directed to download the file rather than opening it, so the user doesn't even leave the page (most of the time).
I am trying to serve up a dynamically generated csv file. For some reason when I get the file, there are 18 empty rows preceding the data. I don't have any space between the headers I define and the csv data I'm sending. If I write the data to a file on the server, it does not get these empty rows. However, if I write the file and then try to serve it to the user, the empty lines come back. So I'm wondering if perhaps I've messed up the headers, or if perhaps there is another issue I'm not thinking of:
function generate_csv($source_type, $include_unpublished = FALSE) {
// retrieve data from DB
....
// start up headers
$csv_name = "$source_type-$data_set-csv_" . date('Y-m-d') . '.csv';
header('Content-Type: text/x-comma-separated-values');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private', false); // required for certain browser
header('Content-Disposition: attachment; filename="' . $csv_name . '"');
// send csv data
print $csv_data;
} //end function
Disclaimer: I asked this question at https://drupal.stackexchange.com/questions/27649/extra-empty-rows-when-serving-csv-file, but it dosn't seem to be drupal-specific and there weren't many ideas coming up over there..
Maybe this lines "hang" in an output buffer, that were started some time before. This way you can set headers without the good old "headers already sent"-error, but this content will be send to the browser when flushing the buffer anyway.
Try
ob_clean();
print $csv_data;
http://php.net/ob-clean
It must be problem with files that you are including. Every whitespace more than one newline after php closing tag ?> is sent to the browser.
Best solution is to get rid of this closing tags in every php file.
Other option will be to remove only unnecessary new lines from them or to bufer output and disregard it before serving file.