phpfiledescriptor

How does `fopen` before `rename` still reads the old content using PHP


The code below opens a file descriptor ($fp) to a specific file, then I overwrite that file (using rename) and then later I read the content of that file using fread. What amazes me is that when reading the file using fread it still points to the content of the original file (before being overwritten)! How does this happen? I think this could only be possible if fopen made an entire copy of the file so it can be read later, but I cant believe fopen makes and entire copy of the file because this would be very inneficient.

<?php

$fileA = "fileA.txt";
$fileB = "fileB.txt";

file_put_contents($fileA,"aaaaaaaaaaaaaa111111",LOCK_EX);
file_put_contents($fileB,"bbbbbbbbbbbbbb222222",LOCK_EX);

$fp = fopen($fileA,"r");

rename($fileB,$fileA);

echo fread($fp,10000);

?>

Amazingly, the code above outputs aaaaaaaaaaaaaa111111 but it should output bbbbbbbbbbbbbb222222. If before fread I use fclose and reopen the file it works as expected (the new content is shown).

My question is how is PHP still being able to show the content of the original file that was already overwritten! I cant believe PHP made an entire copy of the file when I called fopen.

The code above works fine on Linux, however on Windows it will not work because using rename on a file already open with fopen will throw an error - on Linux this seems to be fine, no error.


Solution

  • Please note that the code (for your case) is somewhat OS dependent

    For example, if we slightly change the rename statement to display the rename function result (such as what a commentator has suggested):

    echo "checking result of rename:" . rename($fileB,$fileA);
    

    making the code to become:

    <?php
    
    $fileA = "fileA.txt";
    $fileB = "fileB.txt";
    
    file_put_contents($fileA,"aaaaaaaaaaaaaa111111",LOCK_EX);
    file_put_contents($fileB,"bbbbbbbbbbbbbb222222",LOCK_EX);
    
    
    $fp = fopen($fileA,"r");
    
    echo "checking result of rename:" . rename($fileB,$fileA);
    echo "<br>";
    
    
    echo fread($fp,10000);
    
    ?>
    

    then when running the PHP script on Windows OS, the result (say if run on XAMPP) will be:

    enter image description here

    For the same code, if running the same PHP script on linux OS , the result will be:

    enter image description here

    So for PHP script running on Win OS, since the OS forbids the rename operation (when the file is "opened") , there is no file overwriting operation done when the PHP rename function is triggered, and hence the system faithfully displays the contents of the original fileA.txt, which is "aaaaaaaaaaaaaa111111". (yes I have checked, the rename operation fails and there are still two TXT files there)

    For the PHP script running on linux OS, the rename operation will succeed (even when the fileA.txt is "opened" by fopen), so the PHP overwrites the original fileA.txt with the content of fileB.txt with the data "bbbbbbbbbbbbbb222222".

    However, what you observe is still true, because even for PHP running on linux OS, the system will display "aaaaaaaaaaaaaa111111" when the fileA.txt is already having the data "bbbbbbbbbbbbbb222222" after the rename operation.

    I have tested -- this has nothing to do with whether there is any I/O delay in the rename operation before the fread operation . So even if I add a line of "sleep(10)" before the fread statement, the system will still display the data "aaaaaaaaaaaaaa111111" even the fileA.txt vanishes and replaced by fileB.txt

    So, for this case, the file handle (on linux) will still point to the original file data even if you use the rename function to "overwrite" the data of the original file. The "overwrite" operation (as stated in the official PHP documentation) is telling you the result of the operation. BUT actually the file handle is still pointing to the original data stream (on the I/O) , so the system still displays the old data.

    Of course, one of the ways to tackle will be a file handle refresh, such as:

    1. Close the file handle after the rename statement
    2. Re-open the fileA thru fopen to get a new file handle

    So the following will be good for PHP running on Linux OS: (it will display "bbbbbbbbbbbbbb222222")

    <?php
    
    $fileA = "fileA.txt";
    $fileB = "fileB.txt";
    
    file_put_contents($fileA,"aaaaaaaaaaaaaa111111",LOCK_EX);
    file_put_contents($fileB,"bbbbbbbbbbbbbb222222",LOCK_EX);
    
    
    $fp = fopen($fileA,"r");
    
    echo "checking result of rename:" . rename($fileB,$fileA);
    echo "<br>";
    
    fclose($fp);
    $fp = fopen($fileA,"r");
    
    echo fread($fp,10000);
    
    ?>