Thomas: Downloadsscript

Hallo,

ich verwende schon mehrere Jahre ein Downloadscript für ein Vortragsarchiv. Die Dateien sind großteils MP3s und ca. zwischen 1 und 50 MB groß. Der prinzipielle Aufbau des Scriptes ist:
1. Prüfen ob die Person berechtigt ist diese Datei runterzuladen (optional)
2. Ausgeben des Headers
3. Lesen/Ausgeben der Datei mit fread/echo
4. Eintragen des Downloads in die Datenbank zum zählen der vollständigen Downloads

Ich mache das ganze über ein Downloadscript und nicht über einen Direktdownload, da (1.) manche Vorträge nicht öffentlich sind und (4.) die Downloadzahlen einen ungefähres Feedback geben sollen welche Vorträge besonders gefragt sind.

Der Teil (2./3.), also das eigentliche Downloadscript gefällt mir nicht so ganz, da es vom prinzipiellen Aufbau her fehleranfällig ist und ich auch die letzten Tage eine Rückmeldung bekam, dass bei einer Person die zweite Hälfte des MP3-Files fehlt (Aber insgesamt sind es momentan ca. 100 Files mit avg. 150 Downloads).

Wie könnte man das Script verbessern oder was wären bessere Ansätze, mit denen man auch eine Zugriffskontrolle (ohne .htaccess) und eine zählmöglichkeit vollständiger Downloads hat?

Gruß
Thomas

Hier ist das momentane Script skizziert:

switch ($file_extension) {
  case "mp3": $ctype="audio/mpeg"; break;
  // ...
}

if (file_is_valid($filename)) {
  // Sende Header
  header("Pragma: public");
  header("Expires: 0");
  header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
  header("Cache-Control: private",false);
  header("Content-Type: $ctype");
  header("Content-Disposition: attachment; filename="".basename($filename)."";");
  header("Content-Transfer-Encoding: binary");
  header("Content-Length: ".@filesize($filename));

// Öffne Datei
  $fh = fopen($filename, 'rb');
  while(!feof($fh)) echo fread($fh, 8192);
  fclose($fh);
}

  1. Hi,

    1. Lesen/Ausgeben der Datei mit fread/echo

    das ist IMHO suboptimal - du baust hier einen Mechanismus nach, den PHP schon fix und fertig eingebaut bereitstellt.

    // Öffne Datei
      $fh = fopen($filename, 'rb');
      while(!feof($fh)) echo fread($fh, 8192);
      fclose($fh);

    Diesen ganzen Block könntest du einfach mit

    readfile($filename);

    ersetzen.

    So long,
     Martin

    --
    Man sollte keinen Senf von sich geben, wenn man nicht auch das Würstchen dazu liefern kann.
    Selfcode: fo:) ch:{ rl:| br:< n4:( ie:| mo:| va:) de:] zu:) fl:{ ss:) ls:µ js:(
    1. Hi Martin,

      1. Lesen/Ausgeben der Datei mit fread/echo

      das ist IMHO suboptimal - du baust hier einen Mechanismus nach, den PHP schon fix und fertig eingebaut bereitstellt.

      // Öffne Datei
        $fh = fopen($filename, 'rb');
        while(!feof($fh)) echo fread($fh, 8192);
        fclose($fh);

      Diesen ganzen Block könntest du einfach mit

      readfile($filename);

      ersetzen.

      Als ich damals das Script gebastelt hatte hatte ich mich gegen readfile und für fread entschieden um mögliche RAM-Limits zu umgehen. Wenn die Datei z.B. 50 MB groß ist und das RAM-Limit pro Script bei 32 MB ist müsste es mit readfile doch crashen, da die Datei erst komplett in eine Variable geschrieben wird oder habe ich das falsch verstanden?

      Gruß
      Thomas

      1. Hallo Thomas,

        readfile($filename);
        Als ich damals das Script gebastelt hatte hatte ich mich gegen readfile und für fread entschieden um mögliche RAM-Limits zu umgehen. Wenn die Datei z.B. 50 MB groß ist und das RAM-Limit pro Script bei 32 MB ist müsste es mit readfile doch crashen, da die Datei erst komplett in eine Variable geschrieben wird

        nein, eben nicht. Das ist ja das Schöne. :-)
        Vielleicht verwechselst du das mit der Variante, den kompletten Dateiinhalt erst mit file_get_contents() ein einen String zu laden und dann mit echo oder print wieder auszugeben.

        oder habe ich das falsch verstanden?

        Möglich. Denn readfile() arbeitet AFAIK ohne nennenswerten Pufferbedarf.

        Ciao,
         Martin

        --
        Lieber Blödeleien als blöde Laien.
        Selfcode: fo:) ch:{ rl:| br:< n4:( ie:| mo:| va:) de:] zu:) fl:{ ss:) ls:µ js:(
        1. Hallo Martin,

          Möglich. Denn readfile() arbeitet AFAIK ohne nennenswerten Pufferbedarf.

          Das verunsichert mich etwas. Ich hatte mich damals über die Kommentare in der PHP-Doku in das Thema eingelesen und da meinen einige, dass das Problem bei readfile durchaus bestünde:
          http://www.php.net/manual/de/function.readfile.php#48683
          so habe ich es ungefähr übernommen. Eine andere (korrigierte) Version steht hier:
          http://www.php.net/manual/de/function.readfile.php#54295

          Meinst du die irren sich?

          Nebenbei meint da auch jemand, dass readfile 55% langsamer wäre wie echo/fread, was bei diesen Datenmengen durchaus etwas ausmachen kann.

          Gruß
          Thomas

          1. Hallo,

            Möglich. Denn readfile() arbeitet AFAIK ohne nennenswerten Pufferbedarf.
            Das verunsichert mich etwas. Ich hatte mich damals über die Kommentare in der PHP-Doku in das Thema eingelesen und da meinen einige, dass das Problem bei readfile durchaus bestünde:
            http://www.php.net/manual/de/function.readfile.php#48683
            so habe ich es ungefähr übernommen. Eine andere (korrigierte) Version steht hier:
            http://www.php.net/manual/de/function.readfile.php#54295

            *grübel*

            Meinst du die irren sich?

            Das will ich nicht ausschließen. Ich könnte mir aber auch gut vorstellen, dass sich das auf ältere PHP-Versionen bezieht; immerhin sind die Kommentare schon von 2005. Genau weiß ich's auch nicht, aber ich habe mit PHP5 (damals noch unter Windows) definitiv schon Dateien auf diese Weise durchgeschleust, die größer waren als mein gesetztes memory limit (Memory: 8MB, Dateien >10MB gingen problemlos).

            Nebenbei meint da auch jemand, dass readfile 55% langsamer wäre wie echo/fread

            Hab ich eben auch gesehen. Das überrascht mich sehr (ich würd's eher umgekehrt erwarten), aber darauf habe ich noch nie geachtet.

            Und ich habe gerade im Quellcode von PHP 5.3.6 nachgesehen. Sowohl fpassthru() als auch readfile() rufen intern die C-Funktion php_stream_passthru auf. Die legt einen lokalen Puffer von 8k Größe an (8192 Bytes) und kopiert dann in einer while-Schleife lauter 8k-Häppchen. Im Gegensatz dazu allokiert fread() einen Puffer in der angegebenen Größe und liest alles in einem Rutsch.

            Damit ist beides erklärt: readfile() oder fpassthru() brauchen tatsächlich so gut wie keinen Speicher, und die Schleifenlösung mit fread() kann schneller als readfile() sein, wenn die gewählte Blockgröße deutlich >8k ist.

            was bei diesen Datenmengen durchaus etwas ausmachen kann.

            Klar, wenn wir von Datenmengen reden, die mehrere Minuten ausmachen ...

            Ciao,
             Martin

            --
            Besteht ein Personalrat aus nur einer Person, erübrigt sich die Trennung nach Geschlechtern.
              (aus einer Info des deutschen Lehrerverbands Hessen)
            Selfcode: fo:) ch:{ rl:| br:< n4:( ie:| mo:| va:) de:] zu:) fl:{ ss:) ls:µ js:(
            1. Hallo Martin,

              Und ich habe gerade im Quellcode von PHP 5.3.6 nachgesehen. Sowohl fpassthru() als auch readfile() rufen intern die C-Funktion php_stream_passthru auf. Die legt einen lokalen Puffer von 8k Größe an (8192 Bytes) und kopiert dann in einer while-Schleife lauter 8k-Häppchen. Im Gegensatz dazu allokiert fread() einen Puffer in der angegebenen Größe und liest alles in einem Rutsch.

              Damit ist beides erklärt: readfile() oder fpassthru() brauchen tatsächlich so gut wie keinen Speicher, und die Schleifenlösung mit fread() kann schneller als readfile() sein, wenn die gewählte Blockgröße deutlich >8k ist.

              Danke für deine Recherche! Ich habe gerade auch mal einen lokalen Test (unter Windows) ausgeführt mit 10s max-execution-time, 2MB limit und einer 44MB-Datei bei einer simulierten 1MBit/s-Leitung. Das hat einwandfrei funktioniert - aber lokal ist eben nicht mit dem Server vergleichbar. Auf dem Server interpretiert er auch das "header("Content-Length: ".@filesize($filename));" richtig und der Browser sagt mir viel lange es noch ungefähr dauern könnte und lokal meint er immer die Restdauer wäre unbekannt.

              Wenn ich jetzt readfile() nehme dürften aber mem-limit und max-execution-time (Beim Server momentan auf 32MB / 30s) keine Probleme machen und weitere halbwegs wahrscheinliche Fehlerquellen sollte es nicht geben, oder?

              Gruß
              Thomas

              1. Hallo,

                Und ich habe gerade im Quellcode von PHP 5.3.6 nachgesehen. [...]
                Danke für deine Recherche!

                nichts zu danken; das hat mich jetzt auch mal interessiert, zumal die Frage schon mehrmals unterschwellig aufkam.

                aber lokal ist eben nicht mit dem Server vergleichbar.

                Das ist richtig, aber wenn du mit einem lokalen Server testest -also im HTTP-Umfeld- dann ist mir nicht klar, warum du derartige Unterschiede bekommst.

                Auf dem Server interpretiert er auch das "header("Content-Length: ".@filesize($filename));" richtig und der Browser sagt mir viel lange es noch ungefähr dauern könnte und lokal meint er immer die Restdauer wäre unbekannt.

                Das ist irgendwie nicht logisch. Hast du mal überprüft, ob beim Client (Browser) ein plausibler Content-Length-Header ankommt? Hast du auf deinem Testsystem mal überprüft, ob filesize() die richtige Größe rauskriegt? Greifst du von deinem lokalen Testsystem aus vielleicht wiederum über HTTP auf externe Ressourcen zu? Dann wäre das Verhalten erklärbar; filesize() ist für HTTP-Zugriffe nicht definiert (auch wenn allow_url_fopen gesetzt ist).

                Wenn ich jetzt readfile() nehme dürften aber mem-limit und max-execution-time (Beim Server momentan auf 32MB / 30s) keine Probleme machen

                Nach allem, was wir bis jetzt gesehen haben, eher nicht.

                und weitere halbwegs wahrscheinliche Fehlerquellen sollte es nicht geben, oder?

                Merke: Es gibt immer eine gravierende Fehlerquelle mehr, als du ahnst. ;-)

                Ciao,
                 Martin

                --
                Eifersucht ist so alt wie die Menschheit: Als Adam einmal spät heimkam, zählte Eva sofort seine Rippen.
                Selfcode: fo:) ch:{ rl:| br:< n4:( ie:| mo:| va:) de:] zu:) fl:{ ss:) ls:µ js:(
  2. Der Teil (2./3.), also das eigentliche Downloadscript gefällt mir nicht so ganz, da es vom prinzipiellen Aufbau her fehleranfällig ist und ich auch die letzten Tage eine Rückmeldung bekam, dass bei einer Person die zweite Hälfte des MP3-Files fehlt (Aber insgesamt sind es momentan ca. 100 Files mit avg. 150 Downloads).

    Ich hab bei mir was ähnliches. Hier sende ich einfach nach Abarbeitung von ein paar DB-Geschichten (z.B. Zählen der Downloads) eine header-location Anweisung mit dem Link zur tatsächlichen Datei. Deren Name habe ich in der DB gespeichert, wo auch die Downloads gezählt werden.

    1. Der Teil (2./3.), also das eigentliche Downloadscript gefällt mir nicht so ganz, da es vom prinzipiellen Aufbau her fehleranfällig ist und ich auch die letzten Tage eine Rückmeldung bekam, dass bei einer Person die zweite Hälfte des MP3-Files fehlt (Aber insgesamt sind es momentan ca. 100 Files mit avg. 150 Downloads).

      Ich hab bei mir was ähnliches. Hier sende ich einfach nach Abarbeitung von ein paar DB-Geschichten (z.B. Zählen der Downloads) eine header-location Anweisung mit dem Link zur tatsächlichen Datei. Deren Name habe ich in der DB gespeichert, wo auch die Downloads gezählt werden.

      Das ist prinzipiell schon eine Möglichkeit, hat für mich aber zwei Nachteile:
      a) Ich verliere die Kontrolle über den Zugriff, da nach Abarbeitung des Scriptes der direkte Link verraten wird und durchaus auch kopiert werden kann. Wenn mir aber ein Referent sagt dass sein Vortrag nur für Besucher des Vortrages online gestellt werden darf und nicht für alle (deshalb brauche ich bei manchen Dateien den Teil (1.)) kommt es blöd wenn irgendwo mal ein Direktlink zur Datei auftaucht.
      b) Ich hatte den Counter mal vor dem Download, da haben mir Download-Manager, Stream-Player ect. teils für völlig verschobene Downloadzahlen gesorgt sodass jede Aussagekraft verschwand (sowohl was die Anzahlen als auch die Verhältnisse anbelagt). Bei der Redirect-Methode wird das vermutlich nicht ganz so kritisch sein, aber die grundsätzliche Gefahr besteht noch.

      1. a) Ich verliere die Kontrolle über den Zugriff, da nach Abarbeitung des Scriptes der direkte Link verraten wird und durchaus auch kopiert werden kann.

        Gut, bei mir geht's nur um ein ungefähres Zählen der Zugriffe. Beim Thema Zugriffskontrolle KÖNNTE es tatsächlich unsicher sein, auch wenn ich bei einem Download bei mir zumindest die eigentliche URL nicht sehe. Ich verwende allerdings auch keinen Downloadmanager. Dort taucht sie dann evtl. auf... Beim normalen Download im Browser sieht man maximal den Namen der Datei. Nicht aber, wo sie sich befindet (auch die Adresszeile ändert sich nicht).

        1. Hallo,

          Beim normalen Download im Browser sieht man maximal den Namen der Datei. Nicht aber, wo sie sich befindet (auch die Adresszeile ändert sich nicht).

          ja, das ist mir bei einigen Browsern auch schon sehr unangenehm aufgefallen. AFAIK ist Opera der einzige, der während des Downloads die komplette Quell-URL anzeigt; alle anderen, mit denen ich bisher näher zu tun hatte, verschweigen diese Information geflissentlich.
          Notfalls muss man halt wget für den Download bemühen, da bekommt man auch alle Informationen.

          So long,
           Martin

          --
          You say, it cannot be love if it isn't for ever.
          But let me tell you: Sometimes, a single scene can be more to remember than the whole play.
          Selfcode: fo:) ch:{ rl:| br:< n4:( ie:| mo:| va:) de:] zu:) fl:{ ss:) ls:µ js:(