mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 17:15:08 +00:00
02946abf85
#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.
102 lines
4.2 KiB
PHP
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);
|
|
}
|
|
}
|