PHP lock text file for editing? - php

I've got a form that writes its input to a textfile.
Would it be possible to lock a text file for editing, and perhaps give a friendly message "the file is edited by another user, please try again later."
I'd like to avoid conflicts if the file has multiple editors at the same time.
Here's how the entry is currently added.
$content = file_get_contents("./file.csv");
$fh = fopen("./file.csv", "w");
fwrite($fh, $date_yy . '-' . $date_mm . '-' . $date_dd . '|' . $address . '|' . $person . '|' . $time_hh . ':' . $time_mm);
fwrite($fh, "\n" . $content);
fclose($fh);
Any thoughts?

You can use flock() function to lock the file. For more see this
Something like:
<?php
$content = file_get_contents("./file.csv");
$fp = fopen("./file.csv", "w"); // open it for WRITING ("w")
if (flock($fp, LOCK_EX))
{
// do your file writes here
fwrite($fh, $date_yy . '-' . $date_mm . '-' . $date_dd . '|' . $address . '|' . $person . '|' . $time_hh . ':' . $time_mm);
fwrite($fh, "\n" . $content);
fclose($fh);
flock($fh, LOCK_UN); // unlock the file
}
?>

In order of desirability:
Use a database.
Use more than one text file.
Use locks:
eg:
$lockwait = 2; // seconds to wait for lock
$waittime = 250000; // microseconds to wait between lock attempts
// 2s / 250000us = 8 attempts.
$myfile = '/path/to/file.txt';
if( $fh = fopen($myfile, 'a') ) {
$waitsum = 0;
// attempt to get exclusive, non-blocking lock
$locked = flock($fh, LOCK_EX | LOCK_NB);
while( !$locked && ($waitsum <= $lockwait) ) {
$waitsum += $waittime/1000000; // microseconds to seconds
usleep($waittime);
$locked = flock($fh, LOCK_EX | LOCK_NB);
}
if( !$locked ) {
echo "Could not lock $myfile for write within $lockwait seconds.";
} else {
// write out your data here
flock($fh, LOCK_UN); // ALWAYS unlock
}
fclose($fh); // ALWAYS close your file handle
} else {
echo "Could not open $myfile";
exit 1;
}

You can use PHP's flock function to lock a file for writing, but that lock won't persist across web requests and doesn't work on NFS mounts (at least in my experience).
Your best be may be to create a token file in the same directory, check for its existence and report an error if it exists.
As with any locking scheme, you're going to have race conditions and locks that remain after the operation has completed, so you'll need a way to mitigate those.
I would recommend creating a hash of the file before editing and storing that value in the lock file. Also send that hash to the client as part of the edit form (so it comes back as data on the commit request). Before writing, compare the passed hash value to the value in the file. If they are the same, commit the data and remove the lock.
If they are different, show an error.

You could try flock — Portable advisory file locking ?
http://php.net/manual/en/function.flock.php

I would just use a simple integer or something like that.
$content = file_get_contents("./file.csv");
$fh = fopen("./file.csv", "w");
$status = 1;
...
if($status == 1){
fwrite($fh, $date_yy . '-' . $date_mm . '-' . $date_dd . '|' . $address . '|' . $person . '|' . $time_hh . ':' . $time_mm);
fwrite($fh, "\n" . $content);
fclose($fh);
$status = 0;
}
else
echo "the file is edited by another user, please try again later.";
Is that what you mean?

Related

Writing on existing files in a filesystem database

I have a function that writes ~120Kb-150Kb HTML and meta data on ~8000 .md files with fixed names every few minutes:
a-agilent-technologies-healthcare-nyse-us-39d4
aa-alcoa-basic-materials-nyse-us-159a
aaau-perth-mint-physical-gold--nyse-us-8ed9
aaba-altaba-financial-services-nasdaq-us-26f5
aac-healthcare-nyse-us-e92a
aadr-advisorshares-dorsey-wright-adr--nyse-us-d842
aal-airlines-industrials-nasdaq-us-29eb
If file does not exist, it generates/writes quite fast.
If however the file exists, it does the same much slower, since the existing file carries ~150KB data.
How do I solve this problem?
Do I generate a new file with a new name in the same directory, and unlink the older file in the for loop?
or do I generate a new folder and write all files then I unlink the previous directory? The problem with this method is that sometimes 90% of files are being rewritten and some remain the same.
Code
This function is being called in a for loop, which you can see it in this link
public static function writeFinalStringOnDatabase($equity_symbol, $md_file_content, $no_extension_filename)
{
/**
*#var is the MD file content with meta and entire HTML
*/
$md_file_content = $md_file_content . ConfigConstants::NEW_LINE . ConfigConstants::NEW_LINE;
$dir = __DIR__ . ConfigConstants::DIR_FRONT_SYMBOLS_MD_FILES; // symbols front directory
$new_filename = EQ::generateFileNameFromLeadingURL($no_extension_filename, $dir);
if (file_exists($new_filename)) {
if (is_writable($new_filename)) {
file_put_contents($new_filename, $md_file_content);
if (EQ::isLocalServer()) {
echo $equity_symbol . " 💚 " . ConfigConstants::NEW_LINE;
}
} else {
if (EQ::isLocalServer()) {
echo $equity_symbol . " symbol MD file is not writable in " . __METHOD__ . " 💔 Maybe, check permissions!" . ConfigConstants::NEW_LINE;
}
}
} else {
$fh = fopen($new_filename, 'wb');
fwrite($fh, $md_file_content);
fclose($fh);
if (EQ::isLocalServer()) {
echo $equity_symbol . " front md file does not exit in " . __METHOD__ . " It's writing on the database now 💛" . ConfigConstants::NEW_LINE;
}
}
}
I haven't programmed in PHP for years, but this question has drawn my interest today. :D
Suggestion
How do I solve this problem?
Do I generate a new file with a new name in the same directory, and unlink the older file in the for loop?
Simply use the 3 amigos fopen(), fwrite() & fclose() again, since fwrite will also overwrite the entire content of an existing file.
if (file_exists($new_filename)) {
if (is_writable($new_filename)) {
$fh = fopen($new_filename,'wb');
fwrite($fh, $md_file_content);
fclose($fh);
if (EQ::isLocalServer()) {
echo $equity_symbol . " 💚 " . ConfigConstants::NEW_LINE;
}
} else {
if (EQ::isLocalServer()) {
echo $equity_symbol . " symbol MD file is not writable in " . __METHOD__ . " 💔 Maybe, check permissions!" . ConfigConstants::NEW_LINE;
}
}
} else {
$fh = fopen($new_filename, 'wb');
fwrite($fh, $md_file_content);
fclose($fh);
if (EQ::isLocalServer()) {
echo $equity_symbol . " front md file does not exit in " . __METHOD__ . " It's writing on the database now 💛" . ConfigConstants::NEW_LINE;
}
}
For the sake of DRY principle:
// It's smart to put the logging and similar tasks in a separate function,
// after you end up writing the same thing over and over again.
public static function log($content)
{
if (EQ::isLocalServer()) {
echo $content;
}
}
public static function writeFinalStringOnDatabase($equity_symbol, $md_file_content, $no_extension_filename)
{
$md_file_content = $md_file_content . ConfigConstants::NEW_LINE . ConfigConstants::NEW_LINE;
$dir = __DIR__ . ConfigConstants::DIR_FRONT_SYMBOLS_MD_FILES; // symbols front directory
$new_filename = EQ::generateFileNameFromLeadingURL($no_extension_filename, $dir);
$file_already_exists = file_exists($new_filename);
if ($file_already_exists && !is_writable($new_filename)) {
EQ::log($equity_symbol . " symbol MD file is not writable in " . __METHOD__ . " 💔 Maybe, check permissions!" . ConfigConstants::NEW_LINE);
} else {
$fh = fopen($new_filename,'wb'); // you should also check whether fopen succeeded
fwrite($fh, $md_file_content); // you should also check whether fwrite succeeded
if ($file_already_exists) {
EQ::log($equity_symbol . " 💚 " . ConfigConstants::NEW_LINE);
} else {
EQ::log($equity_symbol . " front md file does not exit in " . __METHOD__ . " It's writing on the database now 💛" . ConfigConstants::NEW_LINE);
}
fclose($fh);
}
}
Possible cause
tl;dr To much overhead due to the Zend string API being used.
The official PHP manual says:
file_put_contents() is identical to calling fopen(), fwrite() and fclose() successively to write data to a file.
However, if you look at the source code of PHP on GitHub, you can see that the part "writing data" is done slightly different in file_put_contents() and fwrite().
In the fwrite function the raw input data (= $md_file_content) is directly accessed in order to write the buffer data to the stream context:
Line 1171:
ret = php_stream_write(stream, input, num_bytes);
In the file_put_contents function on the other hand the Zend string API is used (which I never heard before).
Here the input data and length is encapsulated for some reason.
Line 662
numbytes = php_stream_write(stream, Z_STRVAL_P(data), Z_STRLEN_P(data));
(The Z_STR.... macros are defined here, if you are interested).
So, my suspicion is that possibly the Zend string API is causing the overhead while using file_put_contents.
side note
At first I thought that every file_put_contents() call creates a new stream context, since the lines related to creating context were also slightly different:
PHP_NAMED_FUNCTION(php_if_fopen) (Reference):
context = php_stream_context_from_zval(zcontext, 0);
PHP_FUNCTION(file_put_contents) (Reference):
context = php_stream_context_from_zval(zcontext, flags & PHP_FILE_NO_DEFAULT_CONTEXT);
However, on closer inspection, the php_stream_context_from_zval call is made effectively with the same params, that is the first param zcontext is null, and since you don't pass any flags to file_put_contents, flags & PHP_FILE_NO_DEFAULT_CONTEXT becomes also 0 and is passed as second param.
So, I guess the default stream context is re-used here on every call. Since it's apparently a stream of type persistent it is not disposed after the php_stream_close() call.
So the Fazit, as the Germans say, is there is apparently either no additional overhead or equally same overhead regarding the creation or reusing a stream context in both cases.
Thank you for reading.

PHP: Why file can only be written the first time and subsequent writes fail?

My code:
function log_message($user = "", $message = "") {
$log_file = LOG_PATH;
// if we can open the file
if($handle = fopen($log_file, "a")) {
$timestamp = strftime("%Y-%m-%d:%H %H:%M:%S", time());
$content = $timestamp . " | [{$user}] | " . $message . "\n";
fwrite($handle, $content);
fclose($handle);
} else {
die("error opening log file: {$log_file}");
}
}
Problem:
The first time I can write to the file no problem but any subsequent writes don't get make it through and falls into the else statement?
Other info:
The site is hosted on godaddy and i have set the privacy permissions on the folder to be writable.
What I tried:
I used the function
file_put_contents($log_file, $content . PHP_EOL, FILE_APPEND | LOCK_EX);
but same result.
I also tried the solution mentioned in this question: How to make sure a file handle has been closed before next operation?

Multiple fopens - only first one works

I've a strange issue I am encountering. Recently I switched from webhosting to a VPS, mainly because of flexibility a VPS provides me with.
I now however need to setup Apache on my own and I'm not to good at doing so.
I've a .php file and I have 2 fopen in it. The first one does it's job but the second one doesn't work for some reason.
I was wondering, is there some php.ini settings I need to make to allow multiple fopen in a file ?
EDIT
Code below:
$fp = fopen('ticket' . $_SESSION['id'] . '.txt', 'a+');
$savestring = "---";
fwrite($fp, $savestring);
fclose($fp);
$_SESSION['total'] = $total;
$fp = fopen('reqs.txt', 'a+');
$savestring = PHP_EOL . "Ticket Nou: " . $_SESSION['id'] . " | Ora: " . $ordertime . " | IP: " . $ip;
fwrite($fp, $savestring);
fclose($fp);
I shortened the $savestrings, in reality they are longer. The issue occurs with the second file, reqs.txt.
try to use flock
I have a file with 4 fopen it didn't work.
after using flock it worked.

Second instance of script runs the exact same code as the first one

This script is supposed to write log files using file locks etc to make sure that scripts running at the same time don't have any read/write complications. I got it off someone on php.net. When I tried to run it twice at the same time, I noticed that it completely ignored the lock file. However, when I ran them consecutively, the lock file worked just fine.
That doesn't make any sense whatsoever. The script just checks if a file exists, and acts based on that. Whether another script is running or not, shouldn't influence it at all. I double checked to make sure the lock file was created in both cases; it was.
So I started to do some testing.
First instance started at 11:21:00 outputs:
Started at: 2012-04-12 11:21:00
Checking if weblog/20120412test.txt.1.wlock exists
Got lock: weblog/20120412test.txt.1.wlock
log file not exists, make new
log file was either appended to or create anew
Wrote: 2012-04-12 11:21:00 xx.xx.xx.xxx "testmsg"
1
Second instance started at 11:21:03 outputs:
Started at: 2012-04-12 11:21:00
Checking if weblog/20120412test.txt.1.wlock exists
Got lock: weblog/20120412test.txt.1.wlock
log file not exists, make new
log file was either appended to or create anew
Wrote: 2012-04-12 11:21:00 xx.xx.xx.xxx "testmsg"
1
So there are two things wrong here. The timestamp, and the fact that the script sais the lock file doesn't exist even though it most certainly does.
It's almost as if the second instance of the script simply outputs what the first one did.
<?php
function Weblog_debug($input)
{
echo $input."<br/>";
}
function Weblog($directory, $logfile, $message)
{
// Created 15 september 2010: Mirco Babin
$curtime = time();
$startedat = date('Y-m-d',$curtime) . "\t" . date('H:i:s', $curtime) . "\t";
Weblog_debug("Started at: $startedat");
$logfile = date('Ymd',$curtime) . $logfile;
//Set directory correctly
if (!isset($directory) || $directory === false)
$directory = './';
if (substr($directory,-1) !== '/')
$directory = $directory . '/';
$count = 1;
while(1)
{
//*dir*/*file*.*count*
$logfilename = $directory . $logfile . '.' . $count;
//*dir*/*file*.*count*.lock
$lockfile = $logfilename . '.wlock';
$lockhandle = false;
Weblog_debug("Checking if $lockfile exists");
if (!file_exists($lockfile))
{
$lockhandle = #fopen($lockfile, 'xb'); //lock handle true if lock file opened
Weblog_debug("Got lock: $lockfile");
}
if ($lockhandle !== false) break; //break loop if we got lock
$count++;
if ($count > 100) return false;
}
//log file exists, append
if (file_exists($logfilename))
{
Weblog_debug("log file exists, append");
$created = false;
$loghandle = #fopen($logfilename, 'ab');
}
//log file not exists, make new
else
{
Weblog_debug("log file not exists, make new");
$loghandle = #fopen($logfilename, 'xb');
if ($loghandle !== false) //Did we make it?
{
$created = true;
$str = '#version: 1.0' . "\r\n" .
'#Fields: date time c-ip x-msg' . "\r\n";
fwrite($loghandle,$str);
}
}
//was log file either appended to or create anew?
if ($loghandle !== false)
{
Weblog_debug("log file was either appended to or create anew");
$str = date('Y-m-d',$curtime) . "\t" .
date('H:i:s', $curtime) . "\t" .
(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '-') . "\t" .
'"' . str_replace('"', '""', $message) . '"' . "\r\n";
fwrite($loghandle,$str);
Weblog_debug("Wrote: $str");
fclose($loghandle);
//Only chmod if new file
if ($created) chmod($logfilename,0644); // Read and write for owner, read for everybody else
$result = true;
}
else
{
Weblog_debug("log file was not appended to or create anew");
$result = false;
}
/**
Sleep & disable unlinking of lock file, both for testing purposes.
*/
//Sleep for 10sec to allow other instance(s) of script to run while this one still in progress.
sleep(10);
//fclose($lockhandle);
//#unlink($lockfile);
return $result;
}
echo Weblog("weblog", "test.txt", "testmsg");
?>
UPDATE:
Here's a simple script that just shows the timestamp. I tried it on a different host so I don't think it's a problem with my server;
<?php
function Weblog_debug($input)
{
echo $input."<br/>";
}
$curtime = time();
$startedat = date('Y-m-d',$curtime) . "\t" . date('H:i:s', $curtime) . "\t";
Weblog_debug("Started at: $startedat");
$timediff = time() - $curtime;
while($timediff < 5)
{
$timediff = time() - $curtime;
}
Weblog_debug("OK");
?>
Again, if I start the second instance of the script while the first is in the while loop, the second script will state it started at the same time as the first.
I can't fricking believe this myself, but it turns out this is just a "feature" in Opera. The script works as intended in Firefox. I kinda wish I tested that before I went all berserk on this but there ya go.

if elseif indie while loop

I am trying to handle a CSV file via php, and I have it working. But there is this one array that I need to change based on a set of conditions.
$file_handle = fopen($path, "r");
while (!feof($file_handle) ) {
$line_of_text = fgetcsv($file_handle, 100000);
if($currency == "US"){
$line_of_text[6] = str_replace ("if_you_find_this","change_to_this",$line_of_text[6]);
$line_of_text[6] = str_replace ("if_you_find_this","change_to_this",$line_of_text[6]);
} elseif($currency == "DE"){
$line_of_text[6] = str_replace ("if_you_find_this","change_to_this",$line_of_text[6]);
$line_of_text[6] = str_replace ("if_you_find_this","change_to_this",$line_of_text[6]);
}else {
echo "Something with currency handling went wrong. Please contact support.";
}
$data .= $line_of_text[0] . "," . $line_of_text[1] . "," . $line_of_text[2] . "," . $line_of_text[4] . "," . $line_of_text[6] . "," . $line_of_text[49] . "," . $line_of_text[51] . "\n";
}
fclose($file_handle);
$new_file_handle = fopen($path, "w");
fwrite($new_file_handle, $data);
It's not throwing any errors, but seems that the whole conditional block is being ignored. Help?
You're not reading from $file_handle anywhere. feof($file_handle) will never be true. As posted, this code should loop forever.
(BTW, using feof like this is generally not how you want to do it, for this reason among others. Better would be something like while (($line = however_you_read($file_handle)) !== FALSE).) Pretty much all the stream-reading functions return FALSE on any error, or on EOF.)
Check file is correctly opened or no.
Write that code in try catch, so if any error then you can find it.
I think you file doesn't contain anything or it doesn't open successfully.

Categories