Ein Portal nimmt große Bewerbungsunterlagen entgegen und prüft vor der Verarbeitung die Dateigröße. Zwischen dieser Prüfung und dem Öffnen liegt ein kurzer Moment, in dem sich die Datei ändern kann. Genau für diesen Moment gibt es eine eigene Funktion.
Im Bild hängt die Eigenschaftsliste direkt an der geöffneten Datei, nicht am Dateinamen. Dieser Unterschied ist der Kern des Themas.
Was PHP fstat() liefert
PHP fstat() gibt Informationen zu einer Datei zurück, die bereits geöffnet ist. Der Aufruf bekommt kein Verzeichnis und keinen Dateinamen, sondern das Handle aus fopen(). Zurück kommt ein Array mit dreizehn Werten, von der Größe über die Zeitstempel bis zur Inode-Nummer.
<?php
$handle = fopen('/var/www/upload/lebenslauf.pdf', 'rb');
$daten = fstat($handle);
echo $daten['size']; // 284915
echo date('d.m.Y H:i', $daten['mtime']); /* 10.08.2026 09:12 */
fclose($handle);
Der Unterschied zu filesize() klingt nach einer Kleinigkeit und ist keine. PHP filesize() und disk_free_space() zum Prüfen von Dateigrößen arbeitet mit dem Pfad und beantwortet die Frage "wie groß ist die Datei, die gerade unter diesem Namen liegt". PHP fstat() beantwortet die Frage "wie groß ist die Datei, die ich geöffnet habe". Wird die Datei zwischendurch ersetzt, sind das zwei verschiedene Dateien.
Die Signatur im Original
Die Beschreibung im Original macht den Unterschied zu den verwandten Funktionen an einer einzigen Stelle fest:
fstat(resource $stream): array|false
/* stream - an open file pointer, as returned by fopen()
return - returns an array with statistics about the file,
or false on error
The function gathers the statistics of the file opened by the given
file pointer. Access, modification and inode change times are part
of the result. Unlike stat() it never resolves a filesystem path,
so a file that was renamed or replaced is still reported correctly. */
Der letzte Satz ist der Grund, warum diese Funktion überhaupt existiert, und er wird im Abschnitt zum Dateitausch praktisch vorgeführt.
Das Rückgabearray im Einzelnen
Die dreizehn Felder stammen unverändert aus dem Systemaufruf, den PHP darunter benutzt. Nicht alle sind auf jedem System aussagekräftig.
| Feld | Bedeutung | Verlässlichkeit |
size | Größe in Bytes | überall |
mtime | letzte Änderung des Inhalts | überall |
ctime | letzte Änderung der Metadaten, etwa der Rechte | Unix |
atime | letzter Zugriff | oft abgeschaltet |
ino | Inode-Nummer, identifiziert die Datei selbst | Unix |
mode | Typ und Rechte als Zahl | Unix |
uid, gid | Eigentümer und Gruppe | unter Windows immer 0 |
nlink, dev, rdev | Anzahl der Verweise, Gerätenummern | Unix |
blksize, blocks | Blockgröße und belegte Blöcke | Unix |
Eine Ausgabe mit print_r() zeigt allerdings 26 Einträge und wirkt zunächst fehlerhaft. Der Grund ist harmlos: PHP liefert jeden Wert zweimal, einmal numerisch indiziert in der Reihenfolge des Systemaufrufs und einmal unter seinem Namen. Für eigenen Code werden ausschließlich die benannten Schlüssel benutzt, die numerischen sind ein Erbe aus C und sagen niemandem etwas.
mtime, atime und ctime richtig lesen
Die drei Zeitstempel werden regelmäßig verwechselt, und eine Verwechslung ist besonders häufig: ctime ist nicht die Erstellungszeit. Das c steht für change und meint die letzte Änderung der Metadaten, also etwa der Zugriffsrechte oder des Eigentümers. Eine Erstellungszeit kennt PHP unter Unix gar nicht.
<?php
$handle = fopen('/tmp/bericht.csv', 'rb');
$d = fstat($handle);
printf("Inhalt geaendert: %s\n", date('d.m.Y H:i:s', $d['mtime']));
printf("Metadaten geaendert:%s\n", date('d.m.Y H:i:s', $d['ctime']));
printf("Zuletzt gelesen: %s\n", date('d.m.Y H:i:s', $d['atime']));
fclose($handle);
Wer eine Sortierung nach Alter baut, nimmt mtime. atime ist auf vielen Servern unbrauchbar, weil die Aktualisierung aus Leistungsgründen abgeschaltet ist.
fstat(), stat() und lstat() im Vergleich
Drei Funktionen mit ähnlichem Namen beantworten drei verschiedene Fragen. Der Unterschied zeigt sich am deutlichsten bei symbolischen Verweisen.
| Funktion | Eingabe | Bei Symlink | Nutzt den Cache |
filesize() | Pfad | folgt dem Verweis | ja |
stat() | Pfad | folgt dem Verweis | ja |
lstat() | Pfad | beschreibt den Verweis selbst | ja |
fstat() | offenes Handle | nicht anwendbar | nein |
Die letzte Spalte ist der praktische Kern. PHP fstat() fragt bei jedem Aufruf das Betriebssystem, während die pfadbasierten Funktionen aus einem Zwischenspeicher antworten können.
Der Stat-Cache und warum filesize() manchmal lügt
PHP merkt sich Dateiinformationen innerhalb einer Anfrage. Wird eine Datei geschrieben und direkt danach mit filesize() gemessen, kann der alte Wert zurückkommen. Das fällt bei kurzen Skripten selten auf und bei langen Verarbeitungsläufen umso härter.
<?php
$pfad = '/tmp/wachsend.log';
file_put_contents($pfad, str_repeat('x', 100));
echo filesize($pfad); // 100
file_put_contents($pfad, str_repeat('x', 500));
echo filesize($pfad); /* evtl. immer noch 100 */
clearstatcache(true, $pfad);
echo filesize($pfad); // 500
/* am offenen Handle gibt es das Problem nicht */
$h = fopen($pfad, 'rb');
echo fstat($h)['size']; // 500
fclose($h);
Der zweite Parameter von clearstatcache() begrenzt das Leeren auf einen Pfad und ist die schonendere Variante. Wer ohnehin ein Handle offen hat, spart sich die ganze Überlegung, denn PHP fstat() liest immer den aktuellen Zustand.
Erkennen, ob die Datei ersetzt wurde
Zwei Prozesse arbeiten auf demselben Pfad, und dazwischen wird umbenannt. Der eine schreibt dann in eine Datei, die unter dem Namen längst nicht mehr erreichbar ist. Sichtbar wird das über die Inode-Nummer: Sie gehört zur Datei, nicht zum Namen.
<?php
$pfad = '/var/log/anwendung.log';
$h = fopen($pfad, 'rb');
$offen = fstat($h)['ino'];
$aktuell = stat($pfad)['ino'];
if ($offen !== $aktuell) {
/* die Datei wurde rotiert, Handle neu oeffnen */
fclose($h);
$h = fopen($pfad, 'rb');
}
Genau dieser Vergleich ist der Grund, warum es PHP fstat() überhaupt gibt. Mit pfadbasierten Funktionen allein lässt sich die Frage nicht beantworten.
Welche der vier Funktionen im konkreten Fall die richtige ist, zeigt die folgende Entscheidung.
flowchart TD
A[Was liegt vor] --> B{Offenes Handle}
B -->|ja| C[fstat]
B -->|nein| D{Symlink selbst gemeint}
D -->|ja| E[lstat]
D -->|nein| F{Nur Groesse noetig}
F -->|ja| G[filesize]
F -->|nein| H[stat]
Sobald ein Handle im Spiel ist, gibt es keinen Grund mehr, über den Pfad zu gehen.
Praxis: Content-Length beim Ausliefern setzen
Beim Ausliefern geschützter Dateien muss die Längenangabe im Header zum tatsächlich gesendeten Inhalt passen. Stimmt sie nicht, bricht der Download beim Empfänger ab oder hängt. Der sichere Weg nimmt beide Angaben aus derselben Quelle.
<?php
$pfad = '/var/www/geschuetzt/handbuch.pdf';
$h = fopen($pfad, 'rb');
if ($h === false) {
http_response_code(404);
exit;
}
$groesse = fstat($h)['size'];
header('Content-Type: application/pdf');
header('Content-Length: ' . $groesse);
header('Content-Disposition: attachment; filename="handbuch.pdf"');
fpassthru($h);
fclose($h);
Die Reihenfolge ist zwingend: erst öffnen, dann messen, dann Header setzen, dann ausgeben. Nach der ersten Ausgabe lässt sich kein Header mehr senden. Wer eine Datei ohne eigene Schleife ausliefert, findet die Alternativen im Tutorial zu PHP readfile() und dem direkten Ausliefern von Dateien.
Praxis: eine wachsende Logdatei beobachten
Ein Auswertungsskript soll neue Zeilen verarbeiten, sobald sie geschrieben werden. Das Handle bleibt offen, und PHP fstat() liefert bei jedem Durchlauf die aktuelle Größe.
<?php
$h = fopen('/var/log/anwendung.log', 'rb');
$letzte = fstat($h)['size'];
fseek($h, $letzte);
for ($i = 0; $i < 60; $i++) {
$jetzt = fstat($h)['size'];
if ($jetzt > $letzte) {
while (($zeile = fgets($h)) !== false) {
echo 'neu: ' . $zeile;
}
$letzte = $jetzt;
}
sleep(1);
}
fclose($h);
Ohne offenes Handle wäre dafür in jedem Durchlauf ein neues Öffnen nötig, dazu ein Leeren des Zwischenspeichers. Woher das Handle kommt und welche Modi es gibt, klärt das Tutorial zu PHP fopen() und dem Öffnen, Lesen und Schreiben von Dateien.
Grenzen: Ströme ohne Dateisystem und Windows
Nicht jedes Handle gehört zu einer echten Datei. Bei php://memory und php://temp füllt PHP fstat() nur einen Teil der Felder sinnvoll; ino, dev und die Zeitstempel stehen dann auf 0. Verlässlich ist dort im Wesentlichen size.
Unter Windows sind uid, gid, ino und blocks ohne Aussage und immer 0. Wer darauf eine Rechteprüfung oder eine Identitätsprüfung baut, baut auf Nullen. Zum Prüfen von Zugriffsrechten sind stattdessen is_readable() und die verwandten Funktionen zuständig, die auf beiden Plattformen funktionieren.
Fazit
PHP fstat() beantwortet die Frage nach Größe, Zeitstempeln und Identität einer Datei, die gerade offen ist. Damit umgeht es zwei Fallen auf einmal: den Stat-Cache und das Zeitfenster zwischen Prüfung und Zugriff.
Drei Punkte bleiben hängen. Benutzt werden ausschließlich die benannten Schlüssel, ctime ist keine Erstellungszeit, und die Inode-Nummer verrät, ob unter dem Pfad noch dieselbe Datei liegt. Für alles, was nur einen Pfad hat, bleiben stat() und filesize() die richtige Wahl.