Alexander (HH): IO bzw. CGI.pm Upload speichern

Beitrag lesen

Moin Moin!

mag ja sein, aber ich will das File nicht kopieren sondern per verschieben. Sprich einen Hardlink anlegen. In perl akzeptiert "link" aber keine Filehandles.

In der darunter liegenden Kernel-Schnittstelle (link(2)) auch nicht.

Tja aber den Grund dafür kann ich nicht nachvollziehen ...

int link(const char * oldpath, const char * newpath)

newpath muß ein Name sein, alles andere würde keinen Sinn machen. Ein Handle bekommt man nur mit einem bereits existierenden File, und genau das will man ja erst anlegen.

oldpath könnte theoretisch ein Handle sein, dieses Handle zeigt über ein paar Umwege auf ein File, und dann könnte man auch von da aus einen zweiten Verzeichniseintrag für das File anlegen.

Ein Design-Prinzip (zumindest für Unixe) ist, den Kernel so einfach wie möglich zu halten und den Rest in die libc zu stopfen. Ein fhlink(int oldfh, const char * newpath) wäre ein klassischer Fall für einen libc-Wrapper.

Nur ist nicht alles, was ein Handle ist, auch ein File. Sockets sind auch Handles und können wie File-Handles benutzt werden, nur haben sie keine Entsprechung im Dateisystem.

Und woher kannst Du ein File-Handle bekommen? Von open(), von create(), oder von dem Prozess, der Deinen Prozess angelegt hat. In den beiden ersten Fällen hast Du den Dateinamen, im letzten Fall geht er Dich nichts an. Wenn Dein Elternprozess gewollt hätte, dass Du den Dateinamen kennst, hätte er ihn Deinem Prozess mitgegeben. fhlink(int oldfh, const char * newpath) ist also arbeitlos.

"Note that passing in files as handles instead of names may lead to loss of information on some operating systems; it is recommended that you use file names whenever possible. "

(Wie Du selbst bemertk hast, ist das der Text für copy().)

Richtig, das spielt auf Dateiattribute (chmod), inode-Nummern und exotisches Zeug wie OS/2 Extended Attributes und NTFS Alternative File Streams an. Nichts, was das CGI-Modul benutzt.

Das OS *kann* keine Kopien anfertigen, das machen Hilfsprogramme wie cp und dd. Und die machen abwechselnd read() und write() auf einen einmal angelegten Buffer. Exakt das macht Perl auch, mit unwesentlich mehr overhead.

Hast dus gebenchmarkt? Na das glaub ich dir jetzt mal.

Kennst Du File::Copy::copy()? Das sollte Dir die ganze Kopiererei abnehmen. Und es kopiert in bis zu 2 MB großen Blöcken.

Mal überlegen, was beim Umkopieren eines Uploads bis 2 MB in aller Regel passiert?

bufsize=min(filesize(src),2*1024*1024);
seek(src,0,0);
/* error-handling */
/* Perl: Go to Next OP */
dest=open("name","wb");
if (!dest) errorHandling();
/* Perl: Go to Next OP */
buf=calloc(bufsize,1);
/* Perl: Go to Next OP */
count=read(src,buf,bufsize);
/* error handling */
/* Perl: Go to Next OP */
write(dest,buf,count);
/* error handling */
close(dest);

read() und write() sind hier die Killer: 2 MByte auf die Platte schreiben dauert, ebenso das Lesen von der Platte. Der Rest sind ein paar lausige if(x!=0)-Abfragen, die in Perl vielleicht ein paar CPU-Zyklen länger dauern.

Wenn Du nur einen Upload hast (wie allgemein üblich), und Dein Server genügend RAM frei hat, liegt der Upload sehr wahrscheinlich noch komplett im Buffer Cache. read() muß also nur 2 MB im RAM kopieren. write() wird auch wieder erst in den Buffer Cache schreiben, 2 MB von RAM zu RAM. Und irgendwann nach close(), wenn gerade nichts besseres zu tun ist oder sync() aufgerufen wurde, schreibt der Server den Buffer Cache in den Cache der Festplatte, die wiederum macht daraus irgendwann ein magnetisches Muster auf der Scheibe.

Die Fh-Klasse in CGI.pm gibt sich alle Mühe, den Namen zu verstecken. $query->tmpFileName($upload_fh) sollte den Namen der Temp-Datei ausgeben, wie ich das auf die Schnelle sehe. Die DESTROY-Methode gibt sich aber alle Mühe, Dateien unter Kontrolle von CGI.pm zu löschen, entweder per Handle oder per Namen.

OK, aber wir sind uns einig das ein umbenennen des Tempfiles immer noch sauberer ist als umkopieren. In einen idealen Welt hätte CGI.pm eine Methode link_tmpfile($dest_path).

Wer sagt Dir denn, dass die Tempfiles auf dem gleichen Dateisystem liegen wie die endgültigen Dateien? /tmp ist gerne mal eine eigene Platte oder sogar mit dem Swap und dem RAM kombiniert (tmpfs unter Linux, Solaris). Und genau da mag CGI sein Zeug gerne hinschreiben.

Dann gewinnst Du gar nichts gegenüber dem direkten copy()-Aufruf, im Gegenteil, Du mußt noch etliche Zyklen verbrennen, um herauszufinden, dass rename() nicht funktionieren wird.

In der Doc steht noch was vom UPLOAD_HOOK und das ein unspezifizierter Parameter $buffer geliefert wird ?!?
Mit Verweis auf Apache::Upload Objekte. Und siehe da , hier gibts ne Methode link().

Hilft Dir aber nicht. Denn es ist kein Apache::Upload-Objekt, es bietet nur eine ÄHNLICHE Schnittstelle.

Ich halte ein move bzw link immer noch für den sauberen Ansatz, beherzige aber Damian Conways Ratschlag erst zu benchmarken bevor man zu optimieren anfängt. Wenn keinen die Kopiererei stört, lass ichs mal gut sein.

Ich hab in den letzten 8 Jahren mit der Kopiermethode jedenfalls keine Performance-Probleme gehabt. Gemault haben die Kunden natürlich immer, aber nicht über Datei-Uploads, sondern über pervers lange laufende Reports (nicht aus meiner Feder).

Alexander

--
Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so".