Files
ignacionelson 02946abf85 Stop the delivery docblock naming nginx as the only local path
#1733 explains its two lifetimes by contrasting a presigned URL with
X-Accel-Redirect, "nginx serves these bytes, now, to this request". That
was true when the branch was written and stopped being true on 1 September,
when FileDelivery gave the local path four methods — auto, nginx, xsendfile
and PHP streaming.

The argument survives intact: every one of those authorises exactly one
response and nothing that outlives it, which is the property the contrast
rests on. Only the naming was stale, and a docblock that says "nginx" to
an operator running Apache reads as "this does not apply to me".

Found resolving the merge, not by the author — the branch predates the
change it collided with.
2026-09-07 19:24:00 -03:00

102 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Files\Delivery;
use App\Modules\Files\Models\File;
use App\Support\ContentDisposition;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\Response;
/**
* A stored file's own bytes, put on the wire for whichever disk it lives
* on.
*
* Every route that hands over a file reaches this after authorizing in
* its own way — a policy, a share token, a public-listing check. It
* authorizes nothing itself, and deliberately knows nothing about who is
* asking. The one thing it knows is the thing each caller kept getting
* wrong on its own: that `$file->disk` decides how the bytes travel.
*
* Local disk: handed to FileDelivery, which decides whether the web
* server sends the bytes or PHP does. Anything else — S3, GCS and
* friends — gets a short-lived presigned URL carrying the disposition,
* which an object store ranges just as well.
*
* That distinction matters most for inline(): a <video> seeking through
* an hour of footage issues a long tail of Range requests. Every local
* delivery method answers those — nginx's static handler on the fast
* path, BinaryFileResponse when PHP is streaming — each dropping the
* Content-Length passed here in favour of the range actually served.
*
* The two paths are not equally revocable, which is why the lifetimes
* below differ. Every local delivery method authorises one response and
* no more — nginx's X-Accel-Redirect, Apache's X-Sendfile, or PHP
* streaming the bytes itself: these bytes, now, to this request, and
* nothing that outlives it. A presigned URL is a bearer
* credential — whoever holds it can fetch the file without passing the
* caller's checks again, and it outlives them: a download cap that is
* spent in the meantime, an expires_at that falls in between, an
* assignment that is withdrawn. Nothing here can revoke one, so the only
* dial is how long it lasts.
*
* A download needs to survive being followed, which is a redirect and a
* request: a minute is generous. A preview is held by the player for as
* long as somebody watches, and each seek outside the buffer is a fresh
* Range request against the same URL, so it keeps the hour. That is the
* trade, stated rather than left in a single number.
*
* Callers of inline() must have established that the mime type is
* inline-safe first; PreviewKind is the allowlist, and the reason there
* is one.
*/
class StoredFileResponse
{
/**
* Long enough for a browser, a download manager or a queued transfer
* to follow the redirect and start the request. An object store
* checks the signature when the request arrives, not while it runs,
* so a transfer that begins inside this window finishes however long
* it takes.
*/
private const DOWNLOAD_LINK_SECONDS = 60;
/**
* A preview is watched, not fetched: the player holds this URL and
* issues a Range request every time somebody seeks past the buffer,
* so it has to outlive the viewing rather than the redirect.
*/
private const PREVIEW_LINK_SECONDS = 3600;
public function __construct(private readonly FileDelivery $delivery) {}
/** Shown in place — a preview. */
public function inline(File $file): Response|RedirectResponse
{
return $this->make($file, ContentDisposition::inline($file->original_name), self::PREVIEW_LINK_SECONDS);
}
/** Handed over — a download. */
public function attachment(File $file): Response|RedirectResponse
{
return $this->make($file, ContentDisposition::attachment($file->original_name), self::DOWNLOAD_LINK_SECONDS);
}
private function make(File $file, string $disposition, int $linkSeconds): Response|RedirectResponse
{
if ($file->disk !== 'files') {
$url = Storage::disk($file->disk)->temporaryUrl(
$file->path,
now()->addSeconds($linkSeconds),
['ResponseContentDisposition' => $disposition],
);
return redirect()->away($url);
}
return $this->delivery->serve($file->path, $file->mime_type, $disposition, $file->size);
}
}