This commit is contained in:
2026-05-03 09:21:13 +02:00
parent 44354e5bea
commit 90e93f0d42
18 changed files with 2831 additions and 347 deletions

View File

@@ -751,24 +751,42 @@ class StoryController extends Controller
], 422);
}
$disk = Storage::disk('public');
$base = 'stories/' . now()->format('Y/m') . '/' . Str::uuid();
$extension = strtolower((string) ($file->guessExtension() ?: $file->extension() ?: 'jpg'));
$originalPath = $base . '/original.' . $extension;
$thumbnailPath = $base . '/thumbnail.webp';
$mediumPath = $base . '/medium.webp';
try {
$this->assertStoryMediaStorageIsAllowed();
} catch (\RuntimeException $e) {
return response()->json([
'message' => $e->getMessage(),
], 422);
}
$stream = fopen($sourcePath, 'rb');
if ($stream === false) {
$raw = file_get_contents($sourcePath);
if ($raw === false || $raw === '') {
return response()->json([
'message' => 'Unable to process uploaded image. Please try again.',
], 422);
}
try {
$disk->put($originalPath, $stream);
} finally {
fclose($stream);
$hash = hash('sha256', $raw);
$originalExtension = strtolower((string) ($file->guessExtension() ?: $file->extension() ?: 'jpg'));
if ($originalExtension === 'jpeg') {
$originalExtension = 'jpg';
}
$originalPath = $this->storyMediaPath('original', $hash, $hash . '.' . $originalExtension);
$thumbnailPath = $this->storyMediaPath('sm', $hash);
$mediumPath = $this->storyMediaPath('md', $hash);
$disk = Storage::disk($this->storyMediaDiskName());
$written = $disk->put($originalPath, $raw, [
'visibility' => 'public',
'CacheControl' => 'public, max-age=31536000, immutable',
'ContentType' => (string) ($file->getMimeType() ?: 'application/octet-stream'),
]);
if ($written !== true) {
return response()->json([
'message' => 'Unable to store uploaded image. Please try again.',
], 500);
}
$storedThumbnails = false;
@@ -781,10 +799,20 @@ class StoryController extends Controller
$image = $manager->read($sourcePath);
$thumb = $image->scaleDown(width: 420);
$disk->put($thumbnailPath, (string) $thumb->encode(new WebpEncoder(82)));
$thumbEncoded = (string) $thumb->encode(new WebpEncoder(82));
$disk->put($thumbnailPath, $thumbEncoded, [
'visibility' => 'public',
'CacheControl' => 'public, max-age=31536000, immutable',
'ContentType' => 'image/webp',
]);
$medium = $image->scaleDown(width: 1200);
$disk->put($mediumPath, (string) $medium->encode(new WebpEncoder(85)));
$mediumEncoded = (string) $medium->encode(new WebpEncoder(85));
$disk->put($mediumPath, $mediumEncoded, [
'visibility' => 'public',
'CacheControl' => 'public, max-age=31536000, immutable',
'ContentType' => 'image/webp',
]);
$storedThumbnails = true;
} catch (\Throwable) {
$storedThumbnails = false;
@@ -792,17 +820,62 @@ class StoryController extends Controller
}
if (! $storedThumbnails) {
$disk->copy($originalPath, $thumbnailPath);
$disk->copy($originalPath, $mediumPath);
$disk->put($thumbnailPath, $raw, [
'visibility' => 'public',
'CacheControl' => 'public, max-age=31536000, immutable',
'ContentType' => (string) ($file->getMimeType() ?: 'application/octet-stream'),
]);
$disk->put($mediumPath, $raw, [
'visibility' => 'public',
'CacheControl' => 'public, max-age=31536000, immutable',
'ContentType' => (string) ($file->getMimeType() ?: 'application/octet-stream'),
]);
}
return response()->json([
'thumbnail_url' => $disk->url($thumbnailPath),
'medium_url' => $disk->url($mediumPath),
'original_url' => $disk->url($originalPath),
'thumbnail_url' => $this->storyMediaPublicUrl($thumbnailPath),
'medium_url' => $this->storyMediaPublicUrl($mediumPath),
'original_url' => $this->storyMediaPublicUrl($originalPath),
]);
}
private function storyMediaDiskName(): string
{
return (string) config('uploads.object_storage.disk', 's3');
}
private function storyMediaPath(string $variant, string $hash, ?string $filename = null): string
{
$cleanVariant = trim($variant, '/');
$cleanHash = strtolower((string) preg_replace('/[^a-f0-9]/', '', $hash));
$file = $filename ?? ($cleanHash . '.webp');
return sprintf(
'stories/%s/%s/%s/%s',
$cleanVariant,
substr($cleanHash, 0, 2),
substr($cleanHash, 2, 2),
ltrim($file, '/')
);
}
private function storyMediaPublicUrl(string $path): string
{
return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/');
}
private function assertStoryMediaStorageIsAllowed(): void
{
if (! app()->environment('production')) {
return;
}
$diskName = $this->storyMediaDiskName();
if (in_array($diskName, ['local', 'public'], true)) {
throw new \RuntimeException('Production story media storage must use object storage, not local/public disks.');
}
}
public function tag(string $tag): View
{
$storyTag = StoryTag::query()->where('slug', $tag)->firstOrFail();