mirror of
https://github.com/snipe/snipe-it.git
synced 2026-04-06 07:47:47 +00:00
When PUBLIC_S3_PROXY=true, public uploads (images, logos, avatars) are served through a proxy controller instead of directly from S3. This allows using a single fully private S3 bucket for all storage, with no public ACLs or direct S3 URLs exposed to the browser. The proxy streams files from the configured public disk with proper cache headers (ETag, Last-Modified, Cache-Control). Disabled by default for full backward compatibility.
75 lines
2.5 KiB
PHP
75 lines
2.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class StorageProxyController extends Controller
|
|
{
|
|
/**
|
|
* Proxy files from the "public" storage disk through the application.
|
|
*
|
|
* When PUBLIC_S3_PROXY is enabled, this serves files that would normally
|
|
* be accessed directly from S3 (images, logos, avatars, etc.), allowing
|
|
* a fully private S3 bucket setup.
|
|
*/
|
|
public function show(string $path): Response|StreamedResponse
|
|
{
|
|
$disk = Storage::disk('public');
|
|
|
|
// The S3 adapter includes the disk's root prefix in generated URLs,
|
|
// but Flysystem also prepends it internally on every operation.
|
|
// Strip it here to avoid double-prefixing.
|
|
$root = trim(config('filesystems.disks.public.root', ''), '/');
|
|
if ($root !== '' && str_starts_with($path, $root . '/')) {
|
|
$path = substr($path, strlen($root) + 1);
|
|
}
|
|
|
|
if (! $disk->exists($path)) {
|
|
abort(404);
|
|
}
|
|
|
|
$mimeType = $disk->mimeType($path) ?: 'application/octet-stream';
|
|
$lastModified = $disk->lastModified($path);
|
|
$etag = md5($path . $lastModified);
|
|
$size = $disk->size($path);
|
|
|
|
if ($this->isNotModified($etag, $lastModified)) {
|
|
return response('', 304)
|
|
->header('ETag', '"' . $etag . '"')
|
|
->header('Cache-Control', 'public, max-age=86400');
|
|
}
|
|
|
|
return new StreamedResponse(function () use ($disk, $path) {
|
|
$stream = $disk->readStream($path);
|
|
fpassthru($stream);
|
|
if (is_resource($stream)) {
|
|
fclose($stream);
|
|
}
|
|
}, 200, [
|
|
'Content-Type' => $mimeType,
|
|
'Content-Length' => $size,
|
|
'ETag' => '"' . $etag . '"',
|
|
'Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT',
|
|
'Cache-Control' => 'public, max-age=86400',
|
|
]);
|
|
}
|
|
|
|
private function isNotModified(string $etag, int $lastModified): bool
|
|
{
|
|
$requestEtag = request()->header('If-None-Match');
|
|
if ($requestEtag && $requestEtag === '"' . $etag . '"') {
|
|
return true;
|
|
}
|
|
|
|
$ifModifiedSince = request()->header('If-Modified-Since');
|
|
if ($ifModifiedSince && strtotime($ifModifiedSince) >= $lastModified) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|