Moin zusammen,
heute mal wieder ein Blogpost mit hoffentlich praktischem Nutzen.
Jeder, der ein bisschen in der Webentwicklung tätig war, wird früher oder später auf ein Problem stoßen, wenn er große Dateien einer bestimmten Nutzergruppe zur Verfügung stellen muss. Da gibt es mehrere Möglichkeiten mit mehr oder weniger großen Nachteilen.
– Per Scriptsprache ausliefern: Kann man machen, aber irgendwann greift ggf. ein eingerichtetes Laufzeitlimit. Für größere Dateien also ungeeignet.
– Dateipfad kryptisch benennen: Naja, Linkweitergabe, auch nicht sonderlich toll.
– Per Webserver-Modul ausliefern: Damit wären wir beim Thema mod_xsendfile. Man sagt dem Modul mit einem Header welche Datei ausgeliefert werden soll. Vorher kann man dann die Rechte prüfen, Downloads zählen, … Nachteil: Das Modul muss installiert sein.
Will der Hoster das Modul nicht installieren ist das natürlich suboptimal. Da das bei mir der Fall war hatte ich vor einigen Jahren mal eine Lösung mit temporären Symlinks entwickelt. Die funktioniert zwar, aber war recht schnell – Entschuldigung – hingerotzt.
Da auch andere Leute an der Lösung interessiert waren, habe ich mich noch mal ein Wochenende hingesetzt und das als PHP-Klasse und WordPress-Plugin neu angefangen. Das Ergebnis und Nutzung stelle ich in diesem Post mal vor.
tl;dr: Die Klasse Plugin findet ihr aktuell nur in einem Github-Repo: https://github.com/muellerlukas/simulatesendfile
Oder auch direkt als WordPress-Plugin: https://github.com/muellerlukas/simulatesendfile_wordpress
Die generelle Funktionsweise von mod_xsendfile ist in etwa so:
1) Der User will eine Datei runterladen und ruft einen Link auf.
2) Das entsprechende Script prüft ggf. die Berechtigung des Downloads, zählt den Download, …
3) Wenn das Script die Datei aufruft schickt es einen Header (je nach Webserver „X-Sendfile“, „X-Accel-Redirect“ oder „X-Lighttpd-Sendfile“) mit dem Pfad zur Datei und beendet die Verarbeitung.
4) Das Modul fängt diesen Header ab und liefert die Datei selbst aus.
5) Somit spielt die Scriptlaufzeit keine Rolle mehr und auch große Dateien können ausgeliefert werden.
Das ist natürlich schön weil man so seine üblichen Prüfroutinen beibehalten kann.
Da mein Hoster dieses Modul nicht installieren wollte, ich aber von der Scriptlaufzeit unabhängig sein wollte, wurde also eine Lösung (zumindest für PHP-Systeme) entwickelt: Die ganz grobe Funktionsweise versuche ich mal zu erläutern.
1) Eine Methode wird als shutdown-Funktion bei PHP registriert, rennt also sobald das Script beendet wird – und damit kurz bevor die Kontrolle an den Webserver zurück geht.
2) Sollte keiner der Header vorhanden sein, dann gibt es nichts zu tun.
3) Ansonsten wird für die Datei ein Ordner mit Zufallswert + Symlink zu der Datei angelegt. Warum nicht direkt ein Symlink mit Zufallsnamen? Easy: So kriegt der Nutzer auch den Dateinamen direkt vorgegeben.
4) Die entsprechenden Header werden nun entfernt und stattdessen eine Weiterleitung zum Symlink vorgenommen. Die Kontrolle ist wieder beim Webserver und der Nutzer kann runterladen.
5) Zusätzlich gibt es einen Garbage-Collector der Symlinks + Ordner älter als 1 Stunde (standardmäßig, änderbar) löscht und somit weiteren Download verhindert
Leider gibt es Software (wie z.B. WooCommerce) mit Prüfung ob das Modul im Apache aktiviert ist. Ist das nicht der Fall wird weiterhin über PHP selbst aufgeliefert. Bei jedem Update patchen war mir zu blöd, also hat sich dafür dann auch ein ziemlich dreckiger Hack entwickelt. Eine Datei die ins Template eingebunden werden muss („dirtyhacks.php“) erstellt die Funktion apache_get_modules() wenn sie nicht existiert und behauptet einfach: „mod_xsendfile ist installiert.“
Das funktioniert so lange wie PHP nicht selbst als Modul installiert ist (was bei den meisten Sharehostern auch nicht der Fall ist). Als Modul gibt es die Funktion und kann somit nicht einfach überschrieben werden. (Okay, mit einer PECL-Extension. Aber wenn ich die installieren kann, dann kann ich auch das Modul installieren.)
Das war’s eigentlich an sich auch schon. Im Repo ist auch eine kurze Readme enthalten mit einem kleinen Beispiel zur Verwendung.
Dazu muss ich allerdings noch sagen: Sowohl die Klasse udn auch das WordPress-Plugin sind aktuell noch in einem sehr frühen Stadium. So gab es noch keine genauen Tests mit Windows-Systemen (ja, da gibt es wieder Unterschiede bei den Symlinks), PHP <7 und nginx & Lighty.
Da ich das Problem auch gerade habe, bin ich zufälig hier gelandet. Habe einen kleinen Webshop auf Basis von Woocommerce und die Seite liegt bei all-inkl.
Das Problem sind die größeren Dateien die, wenn man den Force Download verwendet, schnell zu einem Timeout führen. Da ich kein Programmierer bin, suche ich nach einer Lösung, ohne Programmieren zu müssen…
Kommentar by Thomas — Februar 4, 2019 @ 11:54 am
Moin Thomas,
ich habe für WordPress tatsächlich dafür schon ein Plugin geschrieben: https://github.com/muellerlukas/simulatesendfile_wordpress
Ich schreibe dir dazu noch eine Mail mit der genauen Installation. Irgendein Addon hatte ein Problem damit, deswegen muss man ggf. eine Datei manuell anpassen.
Kommentar by muellerlukas — Februar 5, 2019 @ 4:34 pm
Hey, deine Erklärung des Problems trifft auch genau auf mein Problem zu. Es ist genial, dass du dazu schon eine funktionierende Lösung gebaut hast. Leider bin ich mir bei der Installation des Plugins unsicher. Wenn du eine kurze readme ins Github packen könntest, wäre ich dir super dankbar!
LG
Kommentar by David — August 27, 2019 @ 1:55 pm
Moin David,
freut mich wenn es weiter hilft. Meinst du die Klasse oder das WordPress-Plugin? Beim WordPress-Plugin mache ich das heute Nacht direkt noch.
Wenn die Standardprozedur bei dir nicht funktioniert melde dich morgen auch gerne bei mir per Mail. (Steht im Impressum)
Viele Grüße
Lukas
Kommentar by muellerlukas — August 27, 2019 @ 7:13 pm
Wow, danke für den Einsatz. Ich werde das Plugin direkt ausprobieren. Wenn das ganze so funktioniert, wie beschrieben, hast du mir damit den Allerwertesten gerettet 🙂
Kommentar by David — August 30, 2019 @ 10:38 am