Profile: store covers in object storage (WebP); add covers config; remember artworks categories content-type preference
This commit is contained in:
@@ -7,11 +7,9 @@ use App\Support\CoverUrl;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
|
||||
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
|
||||
use Intervention\Image\Encoders\JpegEncoder;
|
||||
use Intervention\Image\Encoders\PngEncoder;
|
||||
use Intervention\Image\Encoders\WebpEncoder;
|
||||
use Intervention\Image\ImageManager;
|
||||
use RuntimeException;
|
||||
@@ -142,9 +140,9 @@ class ProfileCoverController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
private function storageRoot(): string
|
||||
private function coverDiskName(): string
|
||||
{
|
||||
return rtrim((string) config('uploads.storage_root'), DIRECTORY_SEPARATOR);
|
||||
return (string) config('covers.disk', 's3');
|
||||
}
|
||||
|
||||
private function coverDirectory(string $hash): string
|
||||
@@ -152,15 +150,12 @@ class ProfileCoverController extends Controller
|
||||
$p1 = substr($hash, 0, 2);
|
||||
$p2 = substr($hash, 2, 2);
|
||||
|
||||
return $this->storageRoot()
|
||||
. DIRECTORY_SEPARATOR . 'covers'
|
||||
. DIRECTORY_SEPARATOR . $p1
|
||||
. DIRECTORY_SEPARATOR . $p2;
|
||||
return 'covers/' . $p1 . '/' . $p2;
|
||||
}
|
||||
|
||||
private function coverPath(string $hash, string $ext): string
|
||||
{
|
||||
return $this->coverDirectory($hash) . DIRECTORY_SEPARATOR . $hash . '.' . $ext;
|
||||
return $this->coverDirectory($hash) . '/' . $hash . '.' . $ext;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,6 +164,7 @@ class ProfileCoverController extends Controller
|
||||
private function storeCoverFile(UploadedFile $file): array
|
||||
{
|
||||
$this->assertImageManager();
|
||||
$this->assertStorageIsAllowed();
|
||||
|
||||
$uploadPath = (string) ($file->getRealPath() ?: $file->getPathname());
|
||||
if ($uploadPath === '' || ! is_readable($uploadPath)) {
|
||||
@@ -201,18 +197,26 @@ class ProfileCoverController extends Controller
|
||||
));
|
||||
}
|
||||
|
||||
$ext = $mime === 'image/jpeg' ? 'jpg' : ($mime === 'image/png' ? 'png' : 'webp');
|
||||
$image = $this->manager->read($raw);
|
||||
$processed = $image->cover(self::TARGET_WIDTH, self::TARGET_HEIGHT, 'center');
|
||||
$ext = 'webp';
|
||||
$encoded = $this->encodeByExtension($processed, $ext);
|
||||
|
||||
$hash = hash('sha256', $encoded);
|
||||
$dir = $this->coverDirectory($hash);
|
||||
if (! File::exists($dir)) {
|
||||
File::makeDirectory($dir, 0755, true);
|
||||
}
|
||||
$disk = Storage::disk($this->coverDiskName());
|
||||
$written = $disk->put($this->coverPath($hash, $ext), $encoded, [
|
||||
'visibility' => 'public',
|
||||
'CacheControl' => 'public, max-age=31536000, immutable',
|
||||
'ContentType' => match ($ext) {
|
||||
'jpg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
default => 'image/webp',
|
||||
},
|
||||
]);
|
||||
|
||||
File::put($this->coverPath($hash, $ext), $encoded);
|
||||
if ($written !== true) {
|
||||
throw new RuntimeException('Unable to store cover image in object storage.');
|
||||
}
|
||||
|
||||
return ['hash' => $hash, 'ext' => $ext];
|
||||
}
|
||||
@@ -220,8 +224,6 @@ class ProfileCoverController extends Controller
|
||||
private function encodeByExtension($image, string $ext): string
|
||||
{
|
||||
return match ($ext) {
|
||||
'jpg' => (string) $image->encode(new JpegEncoder(85)),
|
||||
'png' => (string) $image->encode(new PngEncoder()),
|
||||
default => (string) $image->encode(new WebpEncoder(85)),
|
||||
};
|
||||
}
|
||||
@@ -235,10 +237,7 @@ class ProfileCoverController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $this->coverPath($trimHash, $trimExt);
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
Storage::disk($this->coverDiskName())->delete($this->coverPath($trimHash, $trimExt));
|
||||
}
|
||||
|
||||
private function assertImageManager(): void
|
||||
@@ -249,4 +248,16 @@ class ProfileCoverController extends Controller
|
||||
|
||||
throw new RuntimeException('Image processing is not available on this environment.');
|
||||
}
|
||||
|
||||
private function assertStorageIsAllowed(): void
|
||||
{
|
||||
if (! app()->environment('production')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$diskName = $this->coverDiskName();
|
||||
if (in_array($diskName, ['local', 'public'], true)) {
|
||||
throw new RuntimeException('Production cover storage must use object storage, not local/public disks.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user