Разработка системы медиа-библиотеки для CMS сайта
Медиа-библиотека — централизованное хранилище и интерфейс управления файлами: изображениями, видео, документами. Вместо загрузки одного файла для каждой записи, редактор выбирает из уже загруженных — меньше дублей, меньше затрат на хранение.
Модель данных
media_files (
id, original_name, file_name, disk, path,
mime_type, size,
width, height, -- для изображений
alt, title, caption, -- SEO и доступность
folder_id,
uploaded_by,
created_at, updated_at
)
media_folders (
id, name, parent_id, created_at
)
media_conversions (
id, media_id, name, -- 'thumb', 'medium', 'large'
file_name, width, height, size, created_at
)
Загрузка файлов через presigned URL
Загрузка напрямую с браузера на S3 — без нагрузки на сервер:
// Сервер генерирует presigned URL
$s3 = new S3Client([...]);
$cmd = $s3->getCommand('PutObject', [
'Bucket' => env('AWS_BUCKET'),
'Key' => "uploads/temp/{$uuid}/{$filename}",
'ContentType' => $mimeType
]);
$presigned = $s3->createPresignedRequest($cmd, '+15 minutes');
return ['upload_url' => (string)$presigned->getUri(), 'key' => $key];
// Клиент загружает напрямую на S3
const { upload_url, key } = await getPresignedUrl(file);
await fetch(upload_url, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
// Подтвердить загрузку серверу
await confirmUpload(key, { alt: '', title: file.name });
Генерация превью
После загрузки — асинхронная генерация версий через очередь:
class GenerateImageConversions implements ShouldQueue
{
public function handle(): void
{
$conversions = [
'thumb' => ['width' => 150, 'height' => 150],
'medium' => ['width' => 800, 'height' => null],
'large' => ['width' => 1920, 'height' => null],
'webp' => ['width' => null, 'height' => null, 'format' => 'webp']
];
foreach ($conversions as $name => $params) {
$image = Image::make(Storage::get($this->media->path));
if ($params['width']) {
$image->resize($params['width'], $params['height'], fn($c) => $c->aspectRatio());
}
if (isset($params['format'])) {
$image->encode($params['format'], 85);
}
Storage::put("media/conversions/{$this->media->id}/{$name}", $image->stream());
}
}
}
Интерфейс медиа-библиотеки
Основные возможности UI:
- Сетка файлов с превью (или список)
- Фильтры: тип файла, папка, дата
- Поиск по имени и alt-тексту
- Загрузка drag-and-drop или кнопкой
- Создание папок
- Множественный выбор для массовых операций
- Редактирование alt, title, caption прямо в библиотеке
- Копирование URL в буфер обмена
Виджет выбора медиа для редактора
function MediaPickerButton({ onSelect, multiple = false }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Выбрать из библиотеки</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="max-w-4xl h-[80vh]">
<MediaLibraryGrid
onSelect={(files) => {
onSelect(multiple ? files : files[0]);
setIsOpen(false);
}}
multiple={multiple}
/>
</DialogContent>
</Dialog>
</>
);
}
Очистка неиспользуемых файлов
// Scheduled job: найти файлы без привязок
$unused = MediaFile::whereDoesntHave('usages')
->where('created_at', '<', now()->subDays(30))
->get();
foreach ($unused as $file) {
Storage::delete($file->path);
$file->delete();
}
Срок разработки: 3–4 недели для полной медиа-библиотеки с генерацией превью, папками и виджетом выбора.







