Herzlich willkommen zum SELF-Treffen 2026
vom 24.04. – 26.04.2026
in Halle (Saale)
Benutzer:Felix Riesterer/Segmentierte Uploads
Schon seit geraumer Zeit (bei der Erstellung dieses Artikel sind es schon zehn (10) Jahre) unterstützen die gängigen Browser in ihren JavaScript-Engines das File API, welches es ermöglicht, eine zum Hochladen ausgewählte Datei byteweise zu lesen. Damit kann JavaScript den Dateiinhalt in Portionen zum Webserver hochladen. Das ist besonders dann nützlich, wenn der Server in seinen PHP-Einstellungen (in der Datei php.ini) zu enge Größenbeschränkungen für Dateiuploads hat. Traditionell sind das 2MB - ein oft unverändert übernommener Defaultwert, der seit über fünfundzwanzig (25) Jahren mit PHP ausgeliefert wird und damit viele PHP-Installationen auf den Webservern im Internet schützen soll.
Inhaltsverzeichnis
JavaScript-Logik
Wie immer braucht es einen Fallback, wenn JavaScript nicht verfügbar sein sollte. In diesem Fall wird das Upload-Formular einen einzelnen Dateiupload anbieten. Sollte aber JavaScript verfügbar sein, wird das submit-Event des Formulars gekapert, damit die Seite beim Senden der Formulardaten nicht mehr neu lädt, sondern JavaScript die Datenübertragung mittels fetch umsetzt.
Der erhöhte Komfort mit einer Mehrfachauswahl lässt sich mit JavaScript gut umsetzen, was dann gleich auch eventuelle Mengenbegrenzungen in PHP damit kompensiert.
File()
Das JavaScript-Objekt hinter dem input-Element im Upload-Formular kennt eine Eigenschaft files, welche eine Liste der ausgewählten Dateien enthält:
// <input type="file" name="..."> finden
const fileInput = document.querySelector('input[type="file"]');
console.log(fileInput.files); // FileList []
Es handelt sich deshalb um eine Liste, weil das input-Element mit dem multiple-Attribut eine Mehrfachauswahl ermöglicht.
multiple-Attribut wird bei dem Ansatz, den dieser Artikel verfolgt, von JavaScript nachgerüstet und Mehrfachauswahlen intern verwaltet. Damit wird gewährleistet, dass ohne JavaScript das Formular noch immer wie erwartet funktioniert - wenn auch nur für Dateien, die innerhalb der Größenbegrenzungen des Servers bleiben.Hat der Benutzer auf der Seite noch keine lokalen Dateien für den Upload ausgewählt, ist diese Liste natürlich noch leer. Aber nachdem (mindestens) eine Datei ausgewählt wurde, gibt es in der FileList einen Eintrag mit der Nummer 0 (eventuell noch weitere unter anderen Nummern), der seinerseits nützliche Eigenschaften kennt:
// <input type="file" name="..."> finden
const fileInput = document.querySelector('input[type="file"]');
console.log(fileInput.files[0]); // File
File {
lastModified: 1774952847572
name: "todo.txt"
size: 3349
type: "text/plain"
webkitRelativePath: ""
}
In der Konsolenausgabe können wir nicht nur den ursprünglichen Dateinamen sehen, sondern auch eine Angabe zu size, welche die Dateigröße in Bytes beschreibt. Diese Angabe ist wichtig, weil man nun berechnen kann, in wieviele Häppchen mit welcher Größe man sie aufteilen möchte.
File-Objekt unter der Nummer 0, sondern alle File-Objekte berücksichtigen!Dieses File-Objekt hat nun Zugriff auf die Daten dieser Datei auf dem lokalen Computer. Man kann sich davon nun sozusagen eine Scheibe abschneiden, die einen Teil dieser Dateidaten enthält. Dazu verwendet man den File-Konstruktor, um sich ein (neues!) File-Objekt damit zu machen:
// <input type="file" name="..."> finden
const fileInput = document.querySelector('input[type="file"]');
const chunk = new File(
[fileInput.files[0].slice(20, 30)], // zehn Bytes ab Position 20
"der Dateiname",
{type: fileInput.files[0].type} // weitere Optionen, hier MIME-Typ
);
console.log(chunk);
File {
lastModified: 1776466677511
name: "der Dateiname"
size: 10
type: "text/plain"
webkitRelativePath: ""
}
Man sieht in der Konsolenausgabe gut, dass es sich um eine Datei mit nur zehn Bytes an Daten handelt, die außerdem einen neuen Dateinamen hat. Diese Teildatei könnte man nun mit JavaScript für einen Dateiupload verwenden.
File-Objekt, weil das alte weiterhin benötigt wird und unverändert bleiben muss, um an der Rest der Daten zu gelangen.text/plain) muss allerdings völlig unerheblich sein, weil eine Überprüfung auf unerwünschte Dateitypen ohnehin serverseitig und von dieser Angabe völlig unabhängig erfolgen muss. Das Beispiel dient hier lediglich der Veranschaulichung, was man mit einem neuen File-Objekt alles tun kann.FormData
Will man für einen POST-Request komplexere Datenstrukturen senden, wie sie üblicherweise bei einem Formular mit enctype="multipart/form-data-Angabe übertragen werden, so benötigt man ein FormData-Objekt. Dieses kann dann im body-Teil eines fetch-Aufrufs stehen, um die via POST zu sendenden Daten zu verwalten:
// <input type="file" name="..."> finden
const fileInput = document.querySelector('input[type="file"]');
const chunk = new File(
[fileInput.files[0].slice(160, 170)], // zehn Bytes ab Position 160
fileInput.files[0].name // ursprünglichen Dateinamen beibehalten
);
const fd = new FormData();
// Teildatei: Wert des name-Attributs des fileInputs wird POST-Schlüssel
fd.append(fileInput.name, chunk);
// weitere Schlüssel-Wert-Paare
fd.append("chunk", 17); // laufende Nummer: Teil 17
fd.append("chunks", 42); // Gesamtmenge: 42 Teile
fd.append("chunk-size", 10); // Größe: 10 Bytes (aktuell Unsinn)
// Upload auslösen
fetch(
window.location, // gleiche Seite (Affenformular)
{
method: "POST",
credentials: "same-origin",
body: fd
}
).then(response => {
// Verarbeitung der Antwort des Servers
});
Im obigen Beispiel sieht man, wie man aus einer vom Benutzer ausgewählten Datei ein beliebiges Stück ihrer Daten in eine neue Datei kopieren kann, die nicht auf dem Datenträger gespeichert wird, sondern rein im Speicher des Browsers verbleibt, bis sie als Dateiupload an den Webserver versandt wird. Zu den POST-Daten für die Datei selbst müssen noch weitere Parameter übertragen werden, um dem Server mitzuteilen, welches Teilstück da aktuell hochgeladen wurde. Die Häppchengröße wurde nur für die Veranschaulichung des aktuellen Teilstücks (17 von 42 mit je 10 Bytes Größe) so klein gewählt.
Häppchengröße
Wenn wir also eine Datei in Häppchen an den Webserver übertragen möchten, zerlegen wir sie in Häppchen mit maximal 2MB Datenmenge. Wenn man das JavaScript mit PHP ausliefert, kann man mit PHP gleich die tatsächliche Größenbeschränkung hineinschreiben, was unter Umständen größere Häppchen als 2MB ermöglicht. Es ist zwar prinzipiell möglich, dass ein Serverbetreiber eine noch kleinere Uploadbegrenzung eingestellt hat, jedoch sehr unwahrscheinlich. In einem solchen Fall müsste man entweder den Wert manuell in den JavaScript-Code schreiben und den dortigen Wert für 2MB damit ersetzen, oder man lässt das PHP erledigen, indem man das JavaScript von PHP ausgeben lässt.
Damit das serverseitige Script beurteilen kann, ob beim Upload etwas schiefgelaufen ist, versehen wir jedes Häppchen mit einer laufenden Nummer (Programmierer fangen immer bei 0 zu zählen an!) und geben ihm die Gesamtanzahl an Häppchen mit, in die unsere lokale Datei aufgeteilt wird, sowie die Häppchengröße. Aus Sicht von PHP würde das gemäß obigem Beispiel in den POST-Daten so aussehen:
Array ( [chunk] => 17 [chunks] => 42 [chunk-size] => 10 )
Nun kann das PHP-Script anhand der laufenden Nummer berechnen, welche Datenmenge in der noch unvollständigen Datei auf dem Server enthalten ist und ob das aktuelle Teilstück genau dazu passt.
PHP-Logik
Wenn der POST-Request vom Browser beim Server ankommt, so ist zunächst zu prüfen, ob hier nur ein Häppchen, oder eine vollständige Datei ankommt. Dazu muss man wissen, wie PHP Dateiuploads behandelt.
Böser Dateiname
Fallback ohne JavaScript
Wenn JavaScript nicht verfügbar ist, stehen im $_POST-Array keine Hinweise zu einem Häppchen. Dieser Fall muss unbedingt berücksichtigt werden!
Häppchen oder doch ganze Datei
Selbst wenn eine Datei klein genug ist, um in einem einzigen Request hochgeladen zu werden, so bekommt sie von JavaScript zwingend die Zusatzangaben zu „Teil X/Y mit Größe Z“ mitgeliefert. In diesem Fall wären die Werte „0/1 mit Größe Z“ (Programmierer fangen immer bei 0 zu zählen an!). Ansonsten handelt es sich wirklich nur um einen Teil der hochzuladenden Datei.
Man kann an dieser Stelle einen Überschreib-Schutz implementieren. Bei einem Häppchen darf die Datei nur dann schon existieren, wenn es sich nicht um das erste Häppchen handelt, also der Wert in $_POST['chunk'] 1 oder größer ist.
richtiges Häppchen zur richtigen Zeit
Wenn man ein Häppchen erhalten hat, kann man anhand des Dateinamens in $_FILES[<name-Wert des input-Elements>]['name'] die auf dem Server bereits angelegte Datei ermitteln und ihre momentane Größe mit filesize() ermitteln. Anhand der Häppchengröße in $_POST['chunk-size'] und der laufenden Nummer des Häppchens in $_POST['chunk'] kann man nun prüfen, ob es sich um das passende Häppchen handelt, oder ob da ein Häppchen unterwegs verloren gegangen ist:
$upload_key = 'upload-file'; // Wert aus name-Attribut von <input>
// Upload OK?
if (array_key_exists($upload_key, $_FILES)
&& UPLOAD_ERR_OK === $_FILES[$upload_key]['error']
) {
$upload_file = get_new_filename_for($_FILES['upload-file']['name']);
if (is_file($upload_file) // Datei existiert!
&& (
// kein Überschreiben mit ganzer Datei
!array_key_exists('chunk', $_POST)
|| (
// auch nicht mit Häppchen, wenn ...
array_key_exists('chunk', $_POST)
&& (
// ... erstes Häppchen
$_POST['chunk'] < 1
|| (
// ... nicht erstes Häppchen, aber ...
$_POST['chunk'] > 0
// ... Häppchen passt nicht
&& filesize($upload_file) != $_POST['chunk'] * $_POST['chunk-size']
)
)
)
)
) {
// Fehler: Datei darf nicht überschrieben werden!
} else {
// OK, Daten akzeptieren
}
}
get_new_filename_for() wird hier nicht beschrieben, sondern steht stellvertretend für einen Mechanismus, der den in den Upload-Daten enthaltenen und vom Browser übertragenen ursprünglichen Dateinamen verarbeitet, damit sicher festgestellt werden kann, zu welcher Datei das aktuell übertragene Häppchen gehören soll.
Da dieser Teil stark davon abhängt, wie das PHP-Projekt insgesamt aufgebaut ist und wie ernste Sicherheitsprobleme strukturell gelöst werden, kann hier unmöglich echter Beispielcode angeboten werden.Datei (weiter)schreiben
Hochgeladene Dateien werden von PHP in einem temporären Verzeichnis abgelegt, in dem sie nach Beenden des Prozesses automatisch wieder gelöscht werden. Üblicherweise verwendet man move_uploaded_file(), um die hochgeladene Datei in das Zielverzeichnis zu verschieben. Das ist bei einem Häppchen aber nicht sinnvoll, da es ja nicht als eigene Datei neben den anderen Häppchen im Verzeichnis zu liegen kommen, sondern in den Inhalt einer womöglich schon bestehenden Datei eingefügt werden soll. Deswegen hat es einen Sinn, stattdessen passende Datei-Handles zu verwenden, die mit fopen() zum Lesen und Schreiben geöffnet werden können.
file_get_contents() den Inhalt des Häppchens einlesen, um ihn dann in die Zieldatei zu schreiben, könnte es sein, dass dieses Einlesen den verfügbaren Arbeitsspeicher überfordert, insbesondere dann, wenn zur Beantwortung für diesen Request in PHP noch andere Dinge den Arbeitsspeicher beanspruchen.Um einen Kopiervorgang zu ermöglichen, der die Resourcen schont, ist ein schrittweises Umkopieren der Datei-Daten mit einer moderaten Menge (z.B. 1MB) sinnvoll:
$upload_key = 'upload-file'; // Wert aus name-Attribut von <input>
// Upload OK?
if (array_key_exists($upload_key, $_FILES)
&& UPLOAD_ERR_OK === $_FILES[$upload_key]['error']
) {
$upload_file = get_new_filename_for($_FILES['upload-file']['name']);
if (is_file($upload_file)
// ... Rest siehe oben
) {
// Fehler: Datei darf nicht überschrieben werden!
} else {
// OK, Daten akzeptieren
$source = fopen($_FILES[$upload_key]['tmp_name'], 'rb');
$target = fopen($upload_file,(
// anhängen oder von Anfang beschreiben?
array_key_exists('chunks', $_POST)
&& array_key_exists('chunk', $_POST)
&& $_POST['chunk'] > 0
? 'ab+' // anhängen
: 'wb+' // von Anfang
));
// in Schritten von 1MB Daten kopieren
while ($part = fread($source, 1024*1024)) {
fwrite($target, $part);
}
fclose($source);
fclose($target);
}
}
Unfertige Dateien - unfertige Gedanken
Wie unterscheidet man Dateien auf dem Server, die nie vollständig hochgeladen wurden? Der Firefox-Browser schreibt während des Downloads in eine Datei mit der Endung .part, um erst bei Vollständigkeit der Datei den tatsächlichen Dateinamen zu geben. Das ist mit dem hier vorgestellten Ansatz auch möglich. Wenn der aktuelle Teilupload Nr. 41 von 42 ist, dann ist das der letzte (wir erinnern uns, dass Programmierer bei null zu zählen beginnen) und die Datei damit vollständig. In dem Moment kann man die Datei beliebig umbenennen.
Man kann sich einen Garbage Collector bauen, der von einem Cronjob ausgelöst wird und Dateien daraufhin untersucht, ob sie die Dateiendung haben, die anzeigt, dass die Datei ein unvollständiger Upload ist, und der anhand des letzten Schreibzugriffs auf die Datei entscheidet, dass diese Datei nicht mehr vollständig werden wird und sie daher löscht.
Trennt man den Vorgang eines segmentierten Uploads von dem ohne JavaScript gesteuerten Original, dann kann man fetch-Aufrufe ohne FormData-Objekt POST-Daten senden lassen, die dann auf Serverseite auf völlig eigene Weise verarbeitet werden. Zwar hat man dann zwei grundsätzlich verschiedene Programmteile, aber dafür eine größere Freiheit beim Gestalten von Rückmeldungen. Scheitert z.B. ein regulärer Upload, lädt der Browser die Zielseite, welche eventuelle Rückmeldungen an den Benutzer enthalten kann. Das kann bei jedem fetch-Aufruf prinzipiell auch so sein, dass der Server die Zielseite als HTML-Dokument in seiner Antwort übermittelt. Will man dieser Antwort allerdings irgendwelche Meldungen entnehmen, um sie dem Benutzer anzuzeigen, muss man dieses HTML-Dokument erst parsen und daraufhin untersuchen. Lässt man den Server dagegen mit JSON-Daten antworten, weil Häppchen eben anders verarbeitet werden, als reguläre Uploads, ist das auf PHP-Seite sicherlich mit weniger Rechenleistung verbunden und kann mit JavaScript obendrein direkter ausgewertet werden.
Wenn's eng wird
Da mit dieser Vorgehensweise wirklich sehr große Dateien hochgeladen werden können, kann man sich so schön seinen Speicherplatz auf dem Server befüllen. Wenn das frei für alle im Internet erreichbar ist, kann das im Zweifel schneller geschehen als einem lieb ist. Und wenn man dann noch frei an die hochgeladenen Dateien gelangt, hat man einen Filehoster gebaut, für den man rechtlich auch noch verantwortlich gemacht werden kann. Und damit schließt sich der Kreis zum eingangs erwähnten Sicherheitsrisiko.