File Upload per Legacy Submit und JavaScript FormData

Verschiedene Möglichkeiten und verschiedene Enctypes Dateien hochzuladen, moderne Alternative zu Perl CGI.pm

Meine Weiterentwicklung von Lincoln Steins Library CGI.pm führt über einen neuen Enctype.

Formular zur Demonstration verschiedender Enctypes

Dateien zum Hochladen und Beschreibung:
Möglichkeiten zum Hochladen:

Erläuterungen

Diese Anwendung zeigt verschiedene Möglichkeiten des Datei Hochladen über den Browser. Welche Möglichkkeiten das sind, ist anhand der Beschriftungen der Schaltflächen ersichtlich. Hinweis: Zum Testen können Sie mehrere Dateien auswählen, eine serverseitige Speicherung erfolgt nicht.

Submit: multipart/form-data

Die herkömmliche Methode: für das Formular werden die Attribute enctype="multipart/form-data" und method="POST" konfiguriert und zum Hochladen wird ein Submit ausgelöst. Zurück zum Formular gelangen Sie über den am Browser befindlichen Button.

JavaScript: multipart/form-data

Alter Schrott nur neu verpackt. Das JavaScript Objekt FormData implementiert nur den alten Enctype multipart/form-data, der JS Code ist einfach.



Hinweis: Bei diesem Enctype wird die Längenangabe einer Datei clientseitig nicht ermittelt und auch nicht im Request gesendet! Die Demo zeigt also die Dateilänge wie sie erst nach dem Upload, also serverseitig festgestellt wurde.

JavaScript: multipart/slice-data

Ebenfalls ein einfacher JS Code jedoch mit einem proprietären Enctype multipart/slice-data welcher die zu übertragenden Daten auf eine moderne Art und Weise binary safe serialisiert. Der Vorteil dieses Enctype gegenüber multipart/form-data ist eine wesentlich geringere CPU Lastigkeit beim Parsen desselben serverseitig.



Hinweis: Bei diesem Enctype wird die Längenangabe einer Datei clientseitig festgestellt und auch im Request übermittelt. Neu ist auch, gegenüber multipart/form-data, daß File.lastModified übertragen wird.

Der wesentliche Unterschied zwischen den Enctypes

Beim Enctype multipart/form-data (JavaScript FormData) wird die Größenangabe einer hochzuladenden Datei nicht clientseitig ermittelt und somit im Request nicht mitgesendet. Der proprietäre Enctype multipart/slice-data hingegen nutzt die FileAPI moderner Browser und ermittelt die Dateilänge bereits clientseitig.

Transparenz: In der Anwendung $file->content_length ist dieser Unterschied nicht sichtbar.

Meine Module als Alternative zu CGI.pm

Wesentlich für die serverseitige Verarbeitung übertragener Datenstrukturen ist die Angabe des Enctype aus welchem der Requestheader Content-Type generiert wird. Somit kann der serverseitige Parser eine, über die hier verwendeteten Enctypes, einheitliche Datenstruktur herstellen. Und dies bedeutet für die Abwärtskompatibilität meiner Entwicklungen zu CGI.pm, daß die Methode $cgi->param(), also der auf dieser Methode aufbauende Code, fast unverändert übernommen werden kann.

Neu gegenüber CGI.pm

Ist, daß die param()-Methode Objekte bzw. Instanzen der Klasse xCGI::File liefert, was die weitere Verarbeitung hochgeladener Dateien bestimmt. Beispiel:

# Skalarer Kontext liefert das 1. Dateiobjekt
my $file = $self->param('upspot');

# Getter-Methoden für Attribute
$file->content_type();
$file->content_length();

# Original Filename
$file->filename();

# Dateiinhalt, Overload, quasi __toString()
my $content = $file;

# Neu in multipart/slice-data
$file->mtime;
# liefert die lokale LastModified
# der Datei in Sekunden

# Localtime anschaulich in
$file->mtime_local
# z.B. Wed Dec 11 13:01:42 2013

Wenn <input type="file" multiple> gesetzt wurde, liefert die param()-Methode also mehrere Instanzen.

Beispielcode: Hochladen mehrerer Dateien

Zum Vergleich mit legacy CGI.pm wird der dafür relevante Code gezeigt. Das Beispiel benutzt File::Copy, damit ist der Unterschied geringfügig und beschränkt sich auf die unterschiedliche Handhabe des Dateiobjekts hinsichtlich Dateiname und Dateihandle. Kopiert wird also von Handle zu Handle.

use strict;
use warnings;
use File::Copy qw(copy);

my $control = sub{
    my $self = shift;
    if( my @files = $self->param('upspot') ){
        foreach my $f (@files){
            # neu mit xCGI.pm
            copy( $f->iohandle, $uploaddir.$f->filename ) or die $!;

            # legacy CGI.pm
            copy($f, $uploaddir.$f) or die $!;

            $fh->close;
        }
        $self->{CONTENT} = "Alle Dateien wurden gespeichert!";
    }
    else{
        $self->{CONTENT} = "Keine Dateien, aber alles OK!";
    }
};

Der CODE ist denkbar einfach. Das Uploadfeld hat den Namen upspot und erlaubt eine Mehrfachauswahl (multiple). Zu jeder eingefügten Datei liefert xCGI eine Instanz der Klasse xCGI::File womit z.B. die Methode filename() aufgerufen werden kann, welche den clientseitigen Namen der jeweiligen Datei liefert; mit diesem Namen wird die Datei dann auch gespeichert serverseitig.

Vergleich mit Legacy CGI.pm bezüglich Upload

Grundlage sei ein Dateiobjekt: $file = $cgi->param('uploadfield') wobei $cgi eine Instanz der Klasse CGI ist.

Legacy CGI.pm xCGI.pm + ParseMultipart.pm
Original Dateiname $file $file->filename()
Handle serverseitig $file $file->iohandle()
Dateiinhalt Handle lesen oder kopieren $file
Content-Type $cgi->uploadInfo($file)->{'Content-Type'} $file->content_type()
LastModified Nicht übermittelt $file->mtime()
Sekunden
LastModified Nicht übermittelt $file->mtime_local()
$file->mtime_gm()
like Wed Dec 11 13:01:42 2013
Dateilänge -s $file
serverseitig ermittelt
$file->content_length()

Transparenz und Schichtenmodell

Das Modul xCGI.pm implementiert ein Schichtenmodell und lädt je nach mitgeliefertem Content-Type den entsprechenden Parser. Damit ist die gesamte Übertragungsstecke transparent, d.h., daß bspw. ein Content-Type application/json ohne Weiteres ausgetauscht werden kann gegen den Enctype application/x-www-form-urlencoded ohne daß der CODE zur Datenverarbeitung geändert werden muss: Die Anwendung der param()-Methode ist immer dieselbe. Ebenso wurde clientseitig ein universeller Serializer entwickelt, welcher in einem Zwischenschritt eine universelle Datenstruktur liefert aus welcher beliebig verschiedene Enctypes erzeugt werden können. Selbstverständlich ist auch das Modul xCGI.pm um beliebige Enctypes erweiterbar, womit bei Bedarf relevanter CODE nachgeladen wird.

Aufgrund der geschaffenen Transparenz vom Serialisieren eines Webformulars über den Transportlayer HTTP bis zur vom serverseitigen Parser gelieferten Datenstruktur sind die Enctypes multipart/form-data und multipart/slice-data untereinander austauschbar. Die Serialisierung des Letzeren erfolgt über das Datenmodell Entity/Attribute/Value, in Perl auch bekannt als Hash of Hashes. Der Slice-Begriff ist in Perl ebenfalls geläufig, hier die Kurzform: [{},{}..{}]; die einzelnen Array-Elemente sind Hashreferenzen. Schließlich ist es in Perl auch recht einfach, zwischen Hash und Array zu transformieren.

Schicht 1: Datenerhebung/Anwendung Client

Auf den Browser bezogen sind das in erster Linie Daten aus Formularen. Allgemein jedoch zählen hierzu auch Webservices, RPC (Remote Procedure Call) usw.. Das Ziel dieses Layers besteht darin, die erhobenen Daten in einer bestimmten Datenstuktur zusammenzufassen. In der Regel beschreibt diese Datenstruktur einen abstrakten Datentyp, also ein Array, Objekt oder Hash, was die Zusammengehörigkeit der Daten manifestiert. Natürlich sind da auch primitive Datentypen möglich. JavaScript: request.js::sampleform() erzeugt aus den Feldern eines Formulars ein Array mit Objekten.

Schicht 2: Serialisierung/Enctype

Dieser Layer ist dafür zuständig, daß die Daten transportsicher verpackt werden. Einfach ausgedrückt, wird aus einer Datentruktur eine Datei bzw. Sequenz erzeugt, die sowohl speicherfähig als auch transportfähig ist. Der wahlfreie Zugriff auf die bis dahin gelieferte Datenstruktur geht verloren. Damit dieser nach dem Tranport wiederhergestellt werden kann, wird ein bestimmter Enctype vereinbart den der Serialize-Algorithmus also bestimmt. Ein solcher Enctype ist z.B. multipart/form-data welcher die Daten auch binary safe überträgt. JavaScript: EAV.js::EAV.slice2blob() erzeugt den proprietären Enctype multipart/slice-data.

Schicht 3: Transport/Speicherung

In diesem Layer spielt sich alles auf einem Low Level, auf Byteebene ab. Das T in HTTP steht zwar für Text, übertragen jedoch werden Bytes im Message Body. Der vereinbarte Enctype wird im Requestheader Content-Type übertragen und die Länge des MessageBody im Header Content-Length als Anzahl der gesendeten Bytes. Der MessageBody ist praktisch eine Datei die genausogut auf einer Festplatte gespeichert werden kann. Protokoll: HTTP ist für den Transport zuständig.

Schicht 4: Deserialisierung/Parser

Entsprechend dem vereinbarten Enctype (Content-Type) wird in diesem Layer die in (1) erzeugte Datenstruktur wiederhergestellt. Gleichzeitig werden die einzelnen Bestandteile wieder in den wahlfreien Zugriff (Random Access) gebracht. Zum Beispiel die übertragenen Parameter als Schlüssel-Werte-Paare, wobei der Wert über den namentlichen Schlüssel direkt adressierbar ist. Perl: ParseMultipart stellt die Daten aus dem Enctype multipart/form-data für den wahlfeien Zugriff wieder her, Datenstruktur siehe Demo/Formular.

Schicht 5: Datenverarbeitung/Anwendung serverseitig

Hiermit sind wir in der serverseitigen Anwendung angelangt. Transparenz schließlich heißt, daß die Layer 2, 3 und 4 im Code der Anwendung weder sichtbar sind noch eine Rolle spielen. Die Anwendung kennt also weder den Enctype, noch das Transportprotokoll und auch nicht die Requestmethode.

Was in JavaScript File.name ist serverseitig $file->filename. Und natürlich File als Objekt samt Inhalt und weiterer Attribute. Diese Transparenz war schon immer die Philosophie in Lincoln Steins Library CGI.pm. Eine Weiterentwicklung, die seit Jahren überfällig ist, erfordert natürlich eine gewisse Planung und auch dabei ist ein Schichtenmodell äußerst hilfreich. Und selbstverständlich OOP mit ihren Mächtigkeiten wie Inherit, Overloading, Delegation und Dependency Injection.

Algorithmen und Datenstrukturen

Eine Gegenüberstellung von PHP's Datenstruktur in $_FILE

Array
(
    [upspot] => Array
        (
            [name] => Array
                (
                    [0] => ehmetsklinge.jpg
                    [1] => eichelberg.jpg
                )

            [type] => Array
                (
                    [0] => image/jpeg
                    [1] => image/jpeg
                )

            [tmp_name] => Array
                (
                    [0] => C:\WINDOWS\Temp\phpB.tmp
                    [1] => C:\WINDOWS\Temp\phpC.tmp
                )

            [error] => Array
                (
                    [0] => 0
                    [1] => 0
                )

            [size] => Array
                (
                    [0] => 11761
                    [1] => 17479
                )

        )

)

mit der hier zugrunde liegende Datenstruktur in Perl

'upspot[]' => [
      bless( {
               'content_length' => '17479',
               'content_type' => 'image/jpeg',
               'filename' => 'eichelberg.jpg',
               'iohandle' => bless( \*Symbol::GEN4, 'IO::String' ),
               'mtime' => '1386763302055',
               'mtime_gm' => 'Wed Dec 11 12:01:42 2013',
               'mtime_local' => 'Wed Dec 11 13:01:42 2013',
               'name' => 'upspot[]'
             }, 'xCGI::File' ),
      bless( {
               'content_length' => '11761',
               'content_type' => 'image/jpeg',
               'filename' => 'ehmetsklinge.jpg',
               'iohandle' => bless( \*Symbol::GEN5, 'IO::String' ),
               'mtime' => '1386765476226',
               'mtime_gm' => 'Wed Dec 11 12:37:56 2013',
               'mtime_local' => 'Wed Dec 11 13:37:56 2013',
               'name' => 'upspot[]'
             }, 'xCGI::File' )
    ]

zeigt, welcher Fortschritt mit OOP/Perl möglich ist: Eine Clientseitige Datenstruktur so zu übertragen, daß sie serverseitig genauso aussieht. Natürlich ist sowas auch mit PHP und sicher auch mit anderen Programmiersprachen möglich, nur hat das eben noch keiner gemacht.

Progressive Enhancement

Als das Buzzwort Web 2.0 aufkam schrieb mal jemand: Webanwendungen werden in Zukunft nicht nur wie Desktopanwendungen aussehen sondern auch dementsprechend funktionieren. Das ist also die Grundidee die dahintersteckt: Ein Benutzer arbeitet mit Webanwendungen so wie er es mit Desktopanwendungen gewohnt ist, er wird keinen Unterschied feststellen. So fügt ein Anwender beispielsweise Bilder, Texte und andere Dateien in ein vorhandenes Dokument ein und klickt auf Speichern, ohne daß die ganze Seite damit neu geladen wird: Die Übertragung der Daten findet im Hintergrund statt ohne daß der Anwender damit etwas mitbekommt, sämtliche an diesem Pozess beteiligte Layer sind für den Benutzer unsichtbar. Und auf den Code der Anwendung bezogen, sind diese Layer durchsichtig (transparent).

Transparenz ist die wesentliche Grundlage, Progressive Enhancement ist nur ein neuer Begriff.


Die rein persönlichen Zwecken dienende Seite verwendet funktionsbedingt einen Session-Cookie. Datenschutzerklärung: 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. @: Rolf Rost, Am Stadtgaben 27, 55276 Oppenheim, nmq​rstx-18­@yahoo.de