Zunächst mal: Du kriegst nicht wirklich verläßlich mit, wann der Browser eine Datei fertig geladen hat, zumal dann nicht, wenn eine andere Anwendung im Spiel ist. "Sicher" geht das also nur, wenn Du tatsächlich den Request selbst auf Berechtigung prüfst.
Wie GwenDragon schon schreibt: Ein übliches Verfahren ist, die Datei nicht per Apache ausliefern zu lassen (denn ich vermute, das ist mit "Ablegen im öffentlichen Bereich" gemeint), sondern mit einen Skript. Ein One-Time-Passwort wäre dann auch nicht unbedingt nötig, Du kannst mit der "normalen" Autorisierung Deines passwortgeschützten Bereichs arbeiten. Das Skript prüft die Autorisierung, liest dann die gewünschte Datei und baut die Response zusammen.
Dabei gibt es einiges zu beachten, was der Apache auch so still und leise im Hintergrund macht:
- MIME sniffing hast Du genannt. Du musst allerdings nicht sniffen, das macht Apache auch nicht. Der geht auf eine Datei mit Namen mime.types (konfigurierbar), auf meinem System hat die grade 845 Einträge. Es gibt auch einen Modul MIME::Types, der dabei hilft, aus einer Dateierweiterung den Typ zu ermitteln. Du könntest schon beim Hochladen der Datei prüfen, ob Deine Anwendung dafür einen MIME-Typ kennt und falls nein, den Anwender danach fragen. Ein richtiger Content-Type verhindert den Download-Dialog!
- Der MIME-typ, den Du für eine Datei ermittelst, kann aus Hygienegründen noch auf Verträglichkeit mit dem Accept: Header des Requests geprüft werden. MIME::Types hilft auch dabei. Das Problem stellt sich nicht, wenn Deine Anwendung die Auswahl der anzeigbaren Dateien mit normalen <a href=....>-Elementen anbietet. Es hilft aber beispielsweise gegen Fehler wie <img src="a.pdf">.
- Etwas mühsamer: Moderne PDF-Reader fordern in jedem Request nur "Häppchen" der gesamten Datei an. Das passiert mit dem Range: Header, der auch mehrfach in einem Request vorkommen kann (wenn der Reader z.B. Bookmarks und eine Seite lädt). Es funktioniert zwar, wenn die Anwendung das ignoriert und immer das gesamte Dokument zurückliefert (Plack::App:File macht das), aber es ist alles andere als effizient. Du müsstest also die Datei aufmachen und nur die gewünschten Portionen zur Response zusammenbauen.