Zweckmäßige Alternative zu Enctype multipart/form-data
Zum Hochladen sehr vieler Dateien und Datenmengen von über 1 GB bei 200 Dateien ist dieser Enctype multipart/form-data
völlig ungeeignet. Selbst wenn der gesamte Request-Body temporär auf die Festplatte des Servers geschrieben und dann geparst wird, wäre dieser Prozess extrem CPU-lastig und würde das Zeitlimit des Webservers erreichen, was einen Abbruch zur Folge hätte. Außerdem bietet dieser Enctype keine Möglichkeit, die Last-Modified sowie Größen-Angaben der hochzuladenden Dateien zu erfassen und mitzusenden.
Streamen heißt, daß die Daten byte- oder blockweise aus STDIN gelesen und sofort auf die Festplatte geschrieben werden. Das hier vorgestellte Verfahren kodiert jede Datei als einen Tupel bzw. Tripel mit den Werten name, binary, lastmod
für jede Einzeldatei und somit werden Tupel für Tupel sequentiell aus STDIN gelesen und immer dann wenn ein Tripel komplett ist, wird die Datei geschrieben.
Als Alternative zu multipart/form-data
bleibt die FileAPI moderner Browser. Damit stehen sämtliche Dateieigenschaften sowie die Dateien selbst zur Verfügung, so kommt es nur noch darauf an diese Informationen richtig zu verpacken für den POST. Betrachte untenstehenden JavaScript-Code:
// BufferEncode
// raw ist ein Array mit strings oder Blobs (File)
function BufferEncode( raw ){
this.result = [];
for(let i = 0; i < raw.length; i++){
if( !raw[i] ) raw[i] = '';
let obj = typeof raw[i] == 'string' ? new Blob([raw[i]]) : raw[i];
let ab = new ArrayBuffer(4);
let dv = new DataView(ab);
dv.setUint32(0, obj.size);
this.result.push( new Blob([ab,obj]) );
}
}
function upload(){
let files = document.getElementById('upspot').files;
if(! files.length ) return;
let sam = [];
for(let i = 0; i < files.length; i++){
sam.push( files[i].name ); // Dateiname
sam.push( ""+files[i].lastModified/1000 ); // 1617353322
sam.push( files[i] ); // Dateiinhalt
}
let up = new BufferEncode(sam);
let xhr = new XHR(cb );
xhr.xhr.upload.onprogress = fuse;
xhr.post('%url%?upload=1', new Blob(up.result));
}
Damit die Einzelwerte aus der Bytesequenz wiederhergestellt werden können, bekommt jeder Einzelwert seine Längenangabe als einen 32-Bit-Integer in Network-Order (Big Endian) vorangestellt, das sind genau 4 Bytes. Dieser Stream wird als HTTP-Message-Body per POST gesendet.
Im Wechsel werden immer genau 4 Bytes gelesen und mit der sich daraus ergebenden Längenangabe der Wert selbst. Nach 3 Schleifendurchläufen ist ein Tupel komplett und die Datei wird weggeschrieben. Das Ganze wiederholt sich solange wie Daten aus STDIN
zu lesen sind, die Anzahl der Bytes findet sich in $ENV{CONTENT_LENGTH}
(Abbruchbedingung der Hauptschleife). Der gesamte Code untenstehend:
package Admin::FotoUp;
# Änderung am 9.1.2023
# die eingespielten Bilder werden nach Jahr und Monat verteilt
# z.B. nach c:/fotoarchiv/2022/11
# wenn die Bilder von der Olympus kommen, beginnen die Dateinamen mit P
# das 2. Zeichen ist der Monat in HEX
# in diesem Fall ersetzen wir das P einfach durch das Jahr
use base main;
use strict;
use warnings;
use IO::File;
sub init{
my $self = shift;
$self->eav('title', $self->eav('title')." $ENV{SERVER_NAME}");
$self->{xfiles} = 0;
$self->{log} = []; # dignostics
}
# Loggen diagnostics
sub log{
my $self = shift;
my $value = shift;
push @{$self->{log}}, $value;
}
# die Datei wird blockweise geschrieben
sub bwrite{
my $self = shift;
#$self->log("@_");
my $name = shift;
my $utime = shift;
my $binlen = shift; # Länge aus STDIN zu lesen
my $path = "$self->{FILEDIR}/".$self->eav('dir');
unless( -d $path ){
mkdir $path or die $!;
}
$path = "c:/fotoarchiv" if $ENV{SERVER_NAME} eq "rolfrost";
my $timehunt = $self->localtime($utime);
my $year = sprintf("%04d", $timehunt->{year});
my $month = sprintf("%02d", $timehunt->{mon});
my $mday = sprintf("%02d", $timehunt->{mday});
if(! -d "$path/$year"){ mkdir "$path/$year" or die $! }
if(! -d "$path/$year/$month"){ mkdir "$path/$year/$month" or die $! }
# Name anpassen, Bilder von Olympus beginnen mit P
if( $name =~ /^p/i ){
$name =~ s/.+(\d\d\d\d)\.jpg/$mday\.$month\.$year\_$1\.jpg/i;
}
#die $name;
# Die Datei wird blockweise aus STDIN gelesen und geschrieben
my $fh = IO::File->new();
$fh->open( "$path//$year/$month/$name", O_CREAT|O_TRUNC|O_BINARY|O_RDWR ) or die $!;
#$self->log('tell vor bwrite: '.tell STDIN);
# hier wird gepuffert
my $buflen = 1000000;
my $int = int($binlen/$buflen);
my $rest = $binlen - $int * $buflen;
for(1..$int){
read(STDIN, my $buffer, $buflen);
$fh->print($buffer);
}
read(STDIN, my $buffer, $rest);
$fh->print($buffer);
#$self->log('tell nach bwrite: '.tell STDIN);
$fh->close;
utime($utime, $utime, "$path/$year/$month/$name") or die $!;
$self->{xfiles}++;
}
# Reihenfolge in Tupel: dateiname, utime, binary
# Datei blockweise
sub control{
my $self = shift;
my $i = 0; # Index Tupel
my @tupel = (); # name, utime, content
while( tell(STDIN) < $ENV{CONTENT_LENGTH} ){
read(STDIN, my $bin, 4);
my $binlen = unpack "N", $bin;
read(STDIN, $tupel[$i], $binlen);
$i++;
if( $i == 2 ){
read(STDIN, my $clen, 4);
$self->bwrite(@tupel, unpack("N", $clen));
@tupel = ();
$i = 0;
}
}
$self->{CONTENT} = "UpFiles: $self->{xfiles} OK "; # $self->dumper($self->{log});
}
1;#########################################################################
__DATA__
<script src="/jquery.min.js"></script>
<script src="/request.js"></script>
<script>
function cb(){
cleanup();
pretext(this.response);
}
function cleanup(){
let pro = document.getElementsByTagName('progress');
for(let i = 0; i < pro.length; i++){ pro[i].remove(); }
}
function upload(){
let files = document.getElementById('upspot').files;
if(! files.length ) return;
let sam = [];
for(let i = 0; i < files.length; i++){
sam.push( files[i].name );
sam.push( ""+files[i].lastModified/1000 ); // 1617353322
sam.push( files[i] );
}
let up = new BufferEncode(sam);
let xhr = new XHR(cb );
xhr.xhr.upload.onprogress = fuse;
xhr.post('%url%?upload=1', new Blob(up.result));
}
</script>
<p style="margin:3em"><input type="file" multiple id="upspot"></p>
<p style="margin:3em"><button onClick="upload()">UpLoad!</button></p>
Diese Anwendung habe ich entwickelt um Fotos vom Smartphone auf meinen Server im LAN bzw. WLAN hochzuladen. In der Ersten Version gab es bei mehr als 100 Dateien (ca. 500 MB) Abbrüche was daran lag daß der gesamte Stream zunächst in den Hauptspeicher kopiert wurde. Es war genau dieses Problem welches infolge der Umstellung auf Streaming gelöst werden konnte. Ein Upload von mehr als 200 Dateien (Fotos, Videos), Datenmenge 1 GB, läuft damit unbeaufsichtigt durch und dauert ca. 4 Minuten.
Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. sos@rolfrost.de.