Export large CSV file - php

I have a mysql table where each record can have unlimited custom fields (EAV model, doesn't matter this) and each field can have unlimited options and each option can have unlimited values.
Right now i am trying to built a export tool that will export all these custom fields with their values, that is: name => value pairs for each field. That's not the important part, it's here just to highlight that we're talking about a lot of mysql queries for a single record and that the size of the export will be pretty large.
For each row from my main table i must do around 100 separate sql queries to get the fields, fields options and field options values. These queries are pretty fast because they all use the right indexes, but still we're talking about 100 queries for a single record and i expect to have around 50k records in my main table just to start with.
Right now, what i do is:
set_time_limit(0);
ini_set('memory_limit', '1G');
ini_set("auto_detect_line_endings", true);
$count = $export->count();
$date = date('Y-m-d-H-i-s');
$fileName = CHtml::encode($export->name) .'-'. $date . '.csv';
$processAtOnce = 100;
$rounds = round($count / $processAtOnce);
header("Content-disposition: attachment; filename={$fileName}");
header("Content-Type: text/csv");
$headerSet = false;
for ($i = 0; $i < $rounds; ++$i) {
$limit = $processAtOnce;
$offset = $i * $processAtOnce;
$rows = $export->find($limit, $offset);
if (empty($rows)) {
continue;
}
$outStream = fopen('php://output', 'w');
if (!$headerSet) {
fputcsv($outStream, array_keys($rows[0]), ',', '"');
$headerSet = true;
}
foreach ($rows as $row) {
fputcsv($outStream, array_values($row), ',', '"');
}
echo fgets($outStream);
fclose($outStream);
}
Basically i count all the records and i "paginate" them for export, then run through the pages to avoin loading too many sql results at once.
I am wondering if this is a valid approach? Any thoughts?
My alternative would be to count all the records, split them into "pages" and for each page do an ajax request(recursive function called after the previous request has been made successfully). When doing the ajax request, process maybe 1k records at once(these 1k would also be splitted like in the above example, run internally 10 times with 100 results for example), write them into a temporary directory(like part-1.csv, part-2.csv) and at the end when all the records are processed, create an archive from the folder containing all the csv parts and force the browser to download it then remove it from the server(window.location.href from within the last ajax call).
Is this a good alternative to the above?
Please note, my goal is to limit the amount of memory usage that's why i think the second approach would help me more.
Please let me know what you think.
Thanks.

My final approach is the second one, after a lot of tests i concluded that in my case the second approach is way better in terms of memory usage, even if the time to complete the entire export is longer, that doesn't matter since the GUI will update with live stats about the export and overall is a good user experience while waiting for the export to finish.
These are the steps i took:
1) Load the page and make first ajax request to server.
2) Server will read first 1000 records in batches of 100 records at a time to avoid getting to many results back at once from mysql.
3) The results are written to a file as part-x.csv, where x is the request number sent by ajax.
4) When there are no more records to add to the file, the last ajax call will create the archive, and delete the folder containing the part-x.csv files. The server then will return a json param called "download" which will contain the url to download the file via PHP(fopen + fread + flush + fclose, followed by unlink the archive file)
5) Using the "download" param, the browser will do a window.location.href = json.download and force the file to be downloaded.
I know, it's more work like this, but as i said, the end result seems to be better than just loading all at once in the way i did first time.

Below is the more optimized approach to Export large CSV file ( thanks to #Joe for above code ) -
Make Ajax request in loops to the server. Below will be the AJAX
call process.
The server will read first records in batches of
records ( chunkSize ) at a time to avoid getting too many results back at once from MySQL.
The file exported_file.csv will be open in
write mode in the first request and append mode in subsequent requests.
The results are written to this file. When there are no
more records to add to the file, the js function will send the file to download.
Below is the example JS function -
<script>
var exportedRecords = 0;
var chunkSize = 500; // as per query performance
for( start=0; start <= totalRecords; start += chunkSize){
chunkCSVExport(,0, chunkSize);
}
function chunkCSVExport(start,chunkSize){
requestData['start'] = start;
requestData['limit'] = chunkSize;
jQuery.ajax({
type : "post",
dataType : "json",
url : action,
data : formData,
success: function(response) {
console.log(response);
exportedRecords += chunkSize;
downloadfile();
}
});
}
function downloadfile(){
if(exportedRecords>=totalRecords){
// call download file function here
}
}
</script>
Below is example PHP code -
<?php
$start = $_POST['start']; //added the missing closing single quote
$limit = $_POST['limit'];
if($start==0) {
$handle = fopen( 'file-export.csv', 'w' );
}else{
$handle = fopen( 'file-export.csv', 'a' );
}
// Run The query from start to limit
$results = getresults($query)
if($start==0) {
$headerDisplayed = false;
}else{
$headerDisplayed = true;
}
foreach ( $results as $data ) {
// Add a header row if it hasn't been added yet
if ( !$headerDisplayed ) {
// Use the keys from $data as the titles
fputcsv($handle, $arrHeaders);
$headerDisplayed = true;
}
// Put the data into the stream
fputcsv($handle, $data);
}
// Close the file
fclose($handle);
// Output some stuff for jquery to use
$response = array(
'result' => 'success'
);
echo json_encode($response);
exit;
?>

Thanks for the post Twisted1919 gave me some inspiration. I know this post is a bit old but I thought I would post some code of my solution so far in case it helps anyone else.
It's using some Wordpress functions for the DB queries.
I am replacing your steps 3 and 4 with.
<?php
// if its a fist run truncate the file. else append the file
if($start==0) {
$handle = fopen( 'temp/prod-export'. '.csv', 'w' );
}else{
$handle = fopen( 'temp/prod-export'. '.csv', 'a' );
}
?>
Some basic jQuery
<script>
// do stuff on the form submit
$('#export-form').submit(function(e){
e.preventDefault();
var formData = jQuery('#export-form').serializeObject();
var chunkAndLimit = 1000;
doChunkedExport(0,chunkAndLimit,formData,$(this).attr('action'),chunkAndLimit);
});
// function to trigger the ajax bit
function doChunkedExport(start,limit,formData,action,chunkSize){
formData['start'] = start;
formData['limit'] = limit;
jQuery.ajax({
type : "post",
dataType : "json",
url : action,
data : formData,
success: function(response) {
console.log(response);
if(response.result=='next'){
start = start + chunkSize;
doChunkedExport(start,limit,formData,action,chunkSize);
}else{
console.log('DOWNLOAD');
}
}
});
}
// A function to turn all form data into a jquery object
jQuery.fn.serializeObject = function(){
var o = {};
var a = this.serializeArray();
jQuery.each(a, function() {
if (o[this.name] !== undefined) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value || '');
} else {
o[this.name] = this.value || '';
}
});
return o;
};
</script>
The php bit
<?php
global $wpdb;
$postCols = array(
'post_title',
'post_content',
'post_excerpt',
'post_name',
);
header("Content-type: text/csv");
$start = intval($_POST['start']);
$limit = intval($_POST['limit']);
// check the total results to workout the finish point
$query = "SELECT count(ID) as total FROM `wp_posts` WHERE post_status = 'publish';";
$results = $wpdb->get_row( $query, ARRAY_A );
$totalResults = $results['total'];
$result = 'next';
if( ($start + $limit ) >= $totalResults){
$result = 'finished';
}
// if its a fist run truncate the file. else append the file
if($start==0) {
$handle = fopen( 'temp/prod-export'. '.csv', 'w' );
}else{
$handle = fopen( 'temp/prod-export'. '.csv', 'a' );
}
$cols = implode(',',$postCols);
//The query
$query = "SELECT {$cols} FROM `wp_posts` WHERE post_status = 'publish' LIMIT {$start},{$limit};";
$results = $wpdb->get_results( $query, ARRAY_A );
if($start==0) {
$headerDisplayed = false;
}else{
$headerDisplayed = true;
}
foreach ( $results as $data ) {
// Add a header row if it hasn't been added yet
if ( !$headerDisplayed ) {
// Use the keys from $data as the titles
fputcsv($handle, array_keys($data));
$headerDisplayed = true;
}
// Put the data into the stream
fputcsv($handle, $data);
}
// Close the file
fclose($handle);
// Output some stuff for jquery to use
$response = array(
'result' => $result,
'start' => $start,
'limit' => $limit,
'totalResults' => $totalResults
);
echo json_encode($response);
// Make sure nothing else is sent, our file is done
exit;
?>

Related

How to cache remote video

I want to play video from a remote server. so I write this function.
$remoteFile = 'blabla.com/video_5GB.mp4';
play($remoteFile);
function play($url){
ini_set('memory_limit', '1024M');
set_time_limit(3600);
ob_start();
if (isset($_SERVER['HTTP_RANGE'])) $opts['http']['header'] = "Range: " . $_SERVER['HTTP_RANGE'];
$opts['http']['method'] = "HEAD";
$conh = stream_context_create($opts);
$opts['http']['method'] = "GET";
$cong = stream_context_create($opts);
$out[] = file_get_contents($url, false, $conh);
$out[] = $httap_response_header;
ob_end_clean();
array_map("header", $http_response_header);
readfile($url, false, $cong);
}
The above function works very well in playing videos. But I don't want to burden the remote server
My question is how can I cache video files every 5 hours to my server. if possible, the cache folder contains small files (5MB / 10MB) from remote video
As mentioned in my comment, the following code has been tested only on a small selection of MP4 files. It could probably do with some more work but it does fill your immediate needs as it is.
It uses exec() to spawn a separate process that generates the cache files when they are needed, i.e. on the first request or after 5 hours. Each video must have its own cache folder because the cached chunks are simply called 1, 2, 3, etc. Please see additional comments in the code.
play.php - This is the script that will be called by the users from the browser
<?php
ini_set('memory_limit', '1024M');
set_time_limit(3600);
$remoteFile = 'blabla.com/video_5GB.mp4';
play($remoteFile);
/**
* #param string $url
*
* This will serve the video from the remote url
*/
function playFromRemote($url)
{
ob_start();
$opts = array();
if(isset($_SERVER['HTTP_RANGE']))
{
$opts['http']['header'] = "Range: ".$_SERVER['HTTP_RANGE'];
}
$opts['http']['method'] = "HEAD";
$conh = stream_context_create($opts);
$opts['http']['method'] = "GET";
$cong = stream_context_create($opts);
$out[] = file_get_contents($url, false, $conh);
$out[] = $http_response_header;
ob_end_clean();
$fh = fopen('response.log', 'a');
if($fh !== false)
{
fwrite($fh, print_r($http_response_header, true)."\n\n\n\n");
fclose($fh);
}
array_map("header", $http_response_header);
readfile($url, false, $cong);
}
/**
* #param string $cacheFolder Directory in which to find the cached chunk files
* #param string $url
*
* This will serve the video from the cache, it uses a "completed.log" file which holds the byte ranges of each chunk
* this makes it easier to locate the first chunk of a range request. The file is generated by the cache script
*/
function playFromCache($cacheFolder, $url)
{
$bytesFrom = 0;
$bytesTo = 0;
if(isset($_SERVER['HTTP_RANGE']))
{
//the client asked for a specific range, extract those from the http_range server var
//can take the form "bytes=123-567" or just a from "bytes=123-"
$matches = array();
if(preg_match('/^bytes=(\d+)-(\d+)?$/', $_SERVER['HTTP_RANGE'], $matches))
{
$bytesFrom = intval($matches[1]);
if(!empty($matches[2]))
{
$bytesTo = intval($matches[2]);
}
}
}
//completed log is a json_encoded file containing an array or byte ranges that directly
//correspond with the chunk files generated by the cache script
$log = json_decode(file_get_contents($cacheFolder.DIRECTORY_SEPARATOR.'completed.log'));
$totalBytes = 0;
$chunk = 0;
foreach($log as $ind => $bytes)
{
//find the first chunk file we need to open
if($bytes[0] <= $bytesFrom && $bytes[1] > $bytesFrom)
{
$chunk = $ind + 1;
}
//and while we are at it save the last byte range "to" which is the total number of bytes of all the chunk files
$totalBytes = $bytes[1];
}
if($bytesTo === 0)
{
if($totalBytes === 0)
{
//if we get here then something is wrong with the cache, revert to serving from the remote
playFromRemote($url);
return;
}
$bytesTo = $totalBytes - 1;
}
//calculate how many bytes will be returned in this request
$contentLength = $bytesTo - $bytesFrom + 1;
//send some headers - I have hardcoded MP4 here because that is all I have developed with
//if you are using different video formats then testing and changes will no doubt be required
header('Content-Type: video/mp4');
header('Content-Length: '.$contentLength);
header('Accept-Ranges: bytes');
//Send a header so we can recognise that the content was indeed served by the cache
header('X-Cached-Date: '.(date('Y-m-d H:i:s', filemtime($cacheFolder.DIRECTORY_SEPARATOR.'completed.log'))));
if($bytesFrom > 0)
{
//We are sending back a range so it needs a header and the http response must be 206: Partial Content
header(sprintf('content-range: bytes %s-%s/%s', $bytesFrom, $bytesTo, $totalBytes));
http_response_code(206);
}
$bytesSent = 0;
while(is_file($cacheFolder.DIRECTORY_SEPARATOR.$chunk) && $bytesSent < $contentLength)
{
$cfh = fopen($cacheFolder.DIRECTORY_SEPARATOR.$chunk, 'rb');
if($cfh !== false)
{
//if we are fetching a range then we might need to seek the correct starting point in the first chunk we look at
//this check will be performed on all chunks but only the first one should need seeking so no harm done
if($log[$chunk - 1][0] < $bytesFrom)
{
fseek($cfh, $bytesFrom - $log[$chunk - 1][0]);
}
//read and send data until the end of the file or we have sent what was requested
while(!feof($cfh) && $bytesSent < $contentLength)
{
$data = fread($cfh, 1024);
//check we are not going to be sending too much back and if we are then truncate the data to the correct length
if($bytesSent + strlen($data) > $contentLength)
{
$data = substr($data, 0, $contentLength - $bytesSent);
}
$bytesSent += strlen($data);
echo $data;
}
fclose($cfh);
}
//move to the next chunk
$chunk ++;
}
}
function play($url)
{
//I have chosen a simple way to make a folder name, this can be improved any way you need
//IMPORTANT: Each video must have its own cache folder
$cacheFolder = sha1($url);
if(!is_dir($cacheFolder))
{
mkdir($cacheFolder, 0755, true);
}
//First check if we are currently in the process of generating the cache and so just play from remote
if(is_file($cacheFolder.DIRECTORY_SEPARATOR.'caching.log'))
{
playFromRemote($url);
}
//Otherwise check if we have never completed the cache or it was completed 5 hours ago and if so spawn a process to generate the cache
elseif(!is_file($cacheFolder.DIRECTORY_SEPARATOR.'completed.log') || filemtime($cacheFolder.DIRECTORY_SEPARATOR.'completed.log') + (5 * 60 * 60) < time())
{
//fork the caching to a separate process - the & echo $! at the end causes the process to run as a background task
//and print the process ID returning immediately
//The cache script can be anywhere, pass the location to sprintf in the first position
//A base64 encoded url is passed in as argument 1, sprintf second position
$cmd = sprintf('php %scache.php %s & echo $!', __DIR__.DIRECTORY_SEPARATOR, base64_encode($url));
$pid = exec($cmd);
//with that started we need to serve the request from the remote url
playFromRemote($url);
}
else
{
//if we got this far then we have a completed cache so serve from there
playFromCache($cacheFolder, $url);
}
}
cache.php - This script will be called by play.php via exec()
<?php
//This script expects as argument 1 a base64 encoded url
if(count($argv)!==2)
{
die('Invalid Request!');
}
$url = base64_decode($argv[1]);
//make sure to use the same method of obtaining the cache folder name as the main play script
//or change the code to pass it in as an argument
$cacheFolder = sha1($url);
if(!is_dir($cacheFolder))
{
die('Invalid Arguments!');
}
//double check it is not already running
if(is_file($cacheFolder.DIRECTORY_SEPARATOR.'caching.log'))
{
die('Already Running');
}
//create a file so we know this has started, the file will be removed at the end of the script
file_put_contents($cacheFolder.DIRECTORY_SEPARATOR.'caching.log', date('d/m/Y H:i:s'));
//get rid of the old completed log
if(is_file($cacheFolder.DIRECTORY_SEPARATOR.'completed.log'))
{
unlink($cacheFolder.DIRECTORY_SEPARATOR.'completed.log');
}
$bytesFrom = 0;
$bytesWritten = 0;
$totalBytes = 0;
//this is the size of the chunk files, currently 10MB
$maxSizeInBytes = 10 * 1024 * 1024;
$chunk = 1;
//open the url for binary reading and first chunk for binary writing
$fh = fopen($url, 'rb');
$cfh = fopen($cacheFolder.DIRECTORY_SEPARATOR.$chunk, 'wb');
if($fh !== false && $cfh!==false)
{
$log = array();
while(!feof($fh))
{
$data = fread($fh, 1024);
fwrite($cfh, $data);
$totalBytes += strlen($data); //use actual length here
$bytesWritten += strlen($data);
//if we are on or passed the chunk size then close the chunk and open a new one
//keeping a log of the byte range of the chunk
if($bytesWritten>=$maxSizeInBytes)
{
$log[$chunk-1] = array($bytesFrom,$totalBytes);
$bytesFrom = $totalBytes;
fclose($cfh);
$chunk++;
$bytesWritten = 0;
$cfh = fopen($cacheFolder.DIRECTORY_SEPARATOR.$chunk, 'wb');
}
}
fclose($fh);
$log[$chunk-1] = array($bytesFrom,$totalBytes);
fclose($cfh);
//write the completed log. This is a json encoded string of the chunk byte ranges and will be used
//by the play script to quickly locate the starting chunk of a range request
file_put_contents($cacheFolder.DIRECTORY_SEPARATOR.'completed.log', json_encode($log));
//finally remove the caching log so the play script doesn't think the process is still running
unlink($cacheFolder.DIRECTORY_SEPARATOR.'caching.log');
}

Parse tabulation-based data file php

I have several files to parse (with PHP) in order to insert their respective content in different database tables.
First point : the client gave me 6 files, 5 are CSV with values separated by coma ; The last one do not come from the same database and its content is tabulation-based.
I built a FileParser that uses SplFileObject to execute a method on each line of the file-content (basically, create an Entity with each dataset and persist it to the database, with Symfony2 and Doctrine2).
But I cannot manage to parse the tabulation-based text file with SplFileObject, it does not split the content in lines as I expect it to do...
// In my controller context
$parser = new MyAmazingFileParser();
$parser->parse($filename, $delimitor, function ($data) use ($em) {
$e = new Entity();
$e->setSomething($data[0);
// [...]
$em->persist($e);
});
// In my parser
public function parse($filename, $delimitor = ',', $run = null) {
if (is_callable($run)) {
$handle = new SplFileObject($filename);
$infos = new SplFileInfo($filename);
if ($infos->getExtension() === 'csv') {
// Everything is going well here
$handle->setCsvControl(',');
$handle->setFlags(SplFileObject::DROP_NEW_LINE + SplFileObject::READ_AHEAD + SplFileObject::SKIP_EMPTY + SplFileObject::READ_CSV);
foreach (new LimitIterator($handle, 1) as $data) {
$result = $run($data);
}
} else {
// Why does the Iterator-way does not work ?
$handle->setCsvControl("\t");
// I have tried with all the possible flags combinations, without success...
foreach (new LimitIterator($handle, 1) as $data) {
// It always only gets the first line...
$result = $run($data);
}
// And the old-memory-killing-dirty-way works ?
$fd = fopen($filename, 'r');
$contents = fread($fd, filesize($filename));
foreach (explode("\t", $contents) as $line) {
// Get all the line as I want... But it's dirty and memory-expensive !
$result = $run($line);
}
}
}
}
It is probably related with the horrible formatting of my client's file, but after a long discussion with them, they really cannot get another format for me, for some acceptable reasons (constraints in their side), unfortunately.
The file is currently long of 49459 lines, so I really think the memory is important at this step ; So I have to make the SplFileObject way working, but do not know how.
An extract of the file can be found here :
Data-extract-hosted

Export large rows to Excel document, in small memory footprint

I am using PHPExcel to create an Excel document, using data from a MySQL database. My script must execute in under 512MB of RAM, and I am running into trouble as my export reaches 200k records:
PHP Fatal error: Allowed memory size of...
How can I use PHPExcel to create large documents in as little amount of RAM as possible?
My current code:
// Autoload classes
ProjectConfiguration::registerPHPExcel();
$xls = new PHPExcel();
$xls->setActiveSheetIndex(0);
$i = 0;
$j = 2;
// Write the col names
foreach ($columnas_excel as $columna) {
$xls->getActiveSheet()->setCellValueByColumnAndRow($i,1,$columna);
$xls->getActiveSheet()->getColumnDimensionByColumn($i)->setAutoSize(true);
$i++;
}
// paginate the result from database
$pager = new sfPropelPager('Antecedentes',50);
$pager->setCriteria($query_personas);
$pager->init();
$last_page = $pager->getLastPage();
//write the data to excel object
for($pagina =1; $pagina <= $last_page; $pagina++) {
$pager->setPage($pagina);
$pager->init();
foreach ($pager->getResults() as $persona) {
$i = 0;
foreach ($columnas_excel as $key_col => $columnas) {
$xls->getActiveSheet()->setCellValueByColumnAndRow($i,$j,$persona->getByName($key_col, BasePeer::TYPE_PHPNAME));
$i++;
}
$j++;
}
}
// write the file to the disk
$writer = new PHPExcel_Writer_Excel2007($xls);
$filename = sfConfig::get('sf_upload_dir') . DIRECTORY_SEPARATOR . "$cache.listado_personas.xlsx";
if (file_exists($filename)) {
unlink($filename);
}
$writer->save($filename);
CSV version:
// Write the col names to the file
$columnas_key = array_keys($columnas_excel);
file_put_contents($filename, implode(",", $columnas_excel) . "\n");
//write data to the file
for($pagina =1; $pagina <= $last_page; $pagina++) {
$pager->setPage($pagina);
$pager->init();
foreach ($pager->getResults() as $persona) {
$persona_arr = array();
// make an array
foreach ($columnas_excel as $key_col => $columnas) {
$persona_arr[] = $persona->getByName($key_col, BasePeer::TYPE_PHPNAME);
}
// append to the file
file_put_contents($filename, implode(",", $persona_arr) . "\n", FILE_APPEND | LOCK_EX);
}
}
Still have the problem of RAM when Propel makes requests to the database, it's like Propel, does not release the RAM every time you make a new request. I even tried to create and delete the Pager object in each iteration
Propel has formatters in the Query API, you'll be able to write this kind of code:
<?php
$query = AntecedentesQuery::create()
// Some ->filter()
;
$csv = $query->toCSV();
$csv contains a CSV content you'l be able to render by setting the correct mime-type.
Since it appears you can use a CSV, try pulling 1 record at a time and appending it to your CSV. Don't try to get all 200k records at the same time.
$cursor = mysql_query( $sqlToFetchData ); // get a MySql resource for your query
$fileHandle = fopen( 'data.csv', 'a'); // use 'a' for Append mode
while( $row = mysql_fetch_row( $cursor ) ){ // pull your data 1 record at a time
fputcsv( $fileHandle, $row ); // append the record to the CSV file
}
fclose( $fileHandle ); // clean up
mysql_close( $cursor );
I'm not sure how to transform the CSV into an XLS file, but hopefully this will get you on your way.

file_get_contents create file is not exists

Is there any alternative to file_get_contents that would create the file if it did not exist. I am basically looking for a one line command. I am using it to count download stats for a program. I use this PHP code in the pre-download page:
Download #: <?php $hits = file_get_contents("downloads.txt"); echo $hits; ?>
and then in the download page, I have this.
<?php
function countdownload($filename) {
if (file_exists($filename)) {
$count = file_get_contents($filename);
$handle = fopen($filename, "w") or die("can't open file");
$count = $count + 1;
} else {
$handle = fopen($filename, "w") or die("can't open file");
$count = 0;
}
fwrite($handle, $count);
fclose($handle);
}
$DownloadName = 'SRO.exe';
$Version = '1';
$NameVersion = $DownloadName . $Version;
$Cookie = isset($_COOKIE[str_replace('.', '_', $NameVersion)]);
if (!$Cookie) {
countdownload("unqiue_downloads.txt");
countdownload("unique_total_downloads.txt");
} else {
countdownload("downloads.txt");
countdownload("total_download.txt");
}
echo '<META HTTP-EQUIV=Refresh CONTENT="0; URL='.$DownloadName.'" />';
?>
Naturally though, the user accesses the pre-download page first, so its not created yet. I do not want to add any functions to the pre download page, i want it to be plain and simple and not alot of adding/changing.
Edit:
Something like this would work, but its not working for me?
$count = (file_exists($filename))? file_get_contents($filename) : 0; echo $count;
Download #: <?php
$hits = '';
$filename = "downloads.txt";
if (file_exists($filename)) {
$hits = file_get_contents($filename);
} else {
file_put_contents($filename, '');
}
echo $hits;
?>
you can also use fopen() with 'w+' mode:
Download #: <?php
$hits = 0;
$filename = "downloads.txt";
$h = fopen($filename,'w+');
if (file_exists($filename)) {
$hits = intval(fread($h, filesize($filename)));
}
fclose($h);
echo $hits;
?>
Type juggling like this can lead to crazy, unforeseen problems later. to turn a string to an integer, you can just add the integer 0 to any string.
For example:
$f = file_get_contents('file.php');
$f = $f + 0;
echo is_int($f); //will return 1 for true
however, i second the use of a database instead of a text file for this. there's a few ways to go about it. one way is to insert a unique string into a table called 'download_count' every time someone downloads the file. the query is as easy as "insert into download_count $randomValue" - make sure the index is unique. then, just count the number of rows in this table when you need the count. the number of rows is the download count. and you have a real integer instead of a string pretending to be an integer. or make a field in your 'download file' table that has a download count integer. each file should be in a database with an id anyway. when someone downloads the file, pull that number from the database in your download function, put it into a variable, increment, update table and show it on the client however you want. use PHP with jQuery Ajax to update it asynchronously to make it cool.
i would still use php and jquery.load(file.php) if you insist on using a text file. that way, you can use your text file for storing any kind of data and just load the specific part of the text file using context selectors. the file.php accepts the $_GET request, loads the right portion of the file and reads the number stored in the file. it then increments the number stored in the file, updates the file and sends data back to the client to be displayed any way you want. for example, you can have a div in your text file with an id set to 'downloadcount' and a div with an id for any other data you want to store in this file. when you load file.php, you just send div#download_count along with the filename and it will only load the value stored in that div. this is a killer way to use php and jquery for cool and easy Ajax/data driven apps. not to turn this into a jquery thread, but this is as simple as it gets.
You can use more concise equivalent yours function countdownload:
function countdownload($filename) {
if (file_exists($filename)) {
file_put_contents($filename, 0);
} else {
file_put_contents($filename, file_get_contents($filename) + 1);
}
}

Ajax progress with PHP session

I have an app that processes images and use jQuery to display progress to the user.
I done this with writing to a textfile each time and image is processed and than read this status with a setInterval.
Because no images are actually written in the processing (I do it in PHP's memory) I thought a log.txt would be a solution, but I am not sure about all the fopen and fread's. Is this prone to issues?
I tried also with PHP sessions, but can't seem to get it to work, I don't get why..
HTML:
<a class="download" href="#">request download</a>
<p class="message"></p>
JS:
$('a.download').click(function() {
var queryData = {images : ["001.jpg", "002.jpg", "003.jpg"]};
$("p.message").html("initializing...");
var progressCheck = function() {
$.get("dynamic-session-progress.php",
function(data) {
$("p.message").html(data);
}
);
};
$.post('dynamic-session-process.php', queryData,
function(intvalId) {
return function(data) {
$("p.message").html(data);
clearInterval(intvalId);
}
} (setInterval(progressCheck, 1000))
);
return false;
});
process.php:
// session_start();
$arr = $_POST['images'];
$arr_cnt = count($arr);
$filename = "log.txt";
for ($i = 1; $i <= $arr_cnt; $i++) {
$content = "processing $val ($i/$arr_cnt)";
$handle = fopen($filename, 'w');
fwrite($handle, $content);
fclose($handle);
// $_SESSION['counter'] = $content;
sleep(3); // to mimic image processing
}
echo "<a href='#'>download zip</a>";
progress.php:
// session_start();
$filename = "log.txt";
$handle = fopen($filename, "r");
$contents = fread($handle, filesize($filename));
fclose($handle);
echo $contents;
// echo $_SESSION['counter'];
What if two clients process images at the same time?
You can try adding session_write_close() between setting the new status in the session, so that the new session data is stored, otherwise it will only get stored once your script finishes.
Another solution would be to save the status in memcache or to use a database,
perhaps separate the statuses with a userid or creating an md5 hash on the image data

Categories