1
0

Комментарии, файлы и права проекта

- Система комментариев к задачам с вложенными ответами
- Редактирование и удаление комментариев
- Прикрепление файлов к задачам и комментариям (картинки, архивы до 10 МБ)
- Система прав проекта: админ проекта может удалять чужие комментарии и файлы
- Универсальный класс FileUpload для загрузки файлов
- Защита загрузки: только автор комментария может добавлять файлы
- Каскадное удаление: задача → комментарии → файлы
- Автообновление комментариев в реальном времени
This commit is contained in:
2026-01-15 06:40:47 +07:00
parent 8ac497df63
commit 3bfa1e9e1b
25 changed files with 3353 additions and 904 deletions

View File

@@ -0,0 +1,223 @@
<?php
class FileUpload {
protected static $base_path = __DIR__ . '/../../../public/';
protected static $base_url = '/public/';
// === Маппинг сущностей (все параметры обязательны) ===
protected static $entities = [
'task' => [
'table' => 'cards_task',
'folder' => 'task',
'field' => 'file_img',
'allowed_ext' => ['png', 'jpg', 'jpeg', 'zip', 'rar'],
'archive_ext' => ['zip', 'rar'],
'max_size' => 10 * 1024 * 1024 // 10 MB
],
'comment' => [
'table' => 'comments',
'folder' => 'comment',
'field' => 'file_img',
'allowed_ext' => ['png', 'jpg', 'jpeg', 'zip', 'rar'],
'archive_ext' => ['zip', 'rar'],
'max_size' => 10 * 1024 * 1024 // 10 MB
]
];
// Получение конфигурации сущности
protected static function getConfig($entity_type) {
if (!isset(self::$entities[$entity_type])) {
return null;
}
return self::$entities[$entity_type];
}
// Получение данных сущности из БД
protected static function getEntityData($config, $entity_id) {
return Database::get($config['table'], '*', ['id' => $entity_id]);
}
// Обновление файлов сущности в БД
protected static function updateEntityFiles($config, $entity_id, $files) {
Database::update($config['table'], [
$config['field'] => json_encode($files, JSON_UNESCAPED_UNICODE)
], [
'id' => $entity_id
]);
}
// Генерация уникального имени файла
protected static function getUniqueName($upload_dir, $file_name) {
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
$base_name = pathinfo($file_name, PATHINFO_FILENAME);
$final_name = $file_name;
$counter = 1;
while (file_exists($upload_dir . '/' . $final_name)) {
$final_name = $base_name . '_' . $counter . '.' . $ext;
$counter++;
}
return $final_name;
}
// Форматирование размера файла для ошибки
protected static function formatSize($bytes) {
if ($bytes >= 1048576) {
return round($bytes / 1048576, 1) . ' МБ';
}
return round($bytes / 1024, 1) . ' КБ';
}
// === ЗАГРУЗКА ФАЙЛА ===
public static function upload($entity_type, $entity_id, $file_base64, $file_name) {
// Получаем конфигурацию
$config = self::getConfig($entity_type);
if (!$config) {
return ['success' => false, 'errors' => ['entity' => 'Неизвестный тип сущности']];
}
// Проверяем что сущность существует
$entity = self::getEntityData($config, $entity_id);
if (!$entity) {
return ['success' => false, 'errors' => ['entity' => 'Сущность не найдена']];
}
// Декодируем base64
$file_data = base64_decode(preg_replace('/^data:[^;]+;base64,/', '', $file_base64));
if (!$file_data) {
return ['success' => false, 'errors' => ['file' => 'Ошибка декодирования файла']];
}
// Проверка расширения
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($ext, $config['allowed_ext'])) {
$allowed = strtoupper(implode(', ', $config['allowed_ext']));
return ['success' => false, 'errors' => ['file' => "Разрешены только: $allowed"]];
}
// Проверка размера
$file_size = strlen($file_data);
if ($file_size > $config['max_size']) {
$max_formatted = self::formatSize($config['max_size']);
return ['success' => false, 'errors' => ['file' => "Файл слишком большой. Максимум $max_formatted"]];
}
// Путь к папке
$upload_dir = self::$base_path . $config['folder'] . '/' . $entity_id;
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
// Уникальное имя
$final_name = self::getUniqueName($upload_dir, $file_name);
// Сохранение файла
$file_path = $upload_dir . '/' . $final_name;
if (!file_put_contents($file_path, $file_data)) {
return ['success' => false, 'errors' => ['file' => 'Ошибка сохранения файла']];
}
// Формируем URL
$file_url = self::$base_url . $config['folder'] . '/' . $entity_id . '/' . $final_name;
// Обновляем file_img в базе
$current_files = $entity[$config['field']] ? json_decode($entity[$config['field']], true) : [];
$current_files[] = [
'url' => $file_url,
'name' => $final_name,
'size' => $file_size
];
self::updateEntityFiles($config, $entity_id, $current_files);
return [
'success' => true,
'file' => [
'url' => $file_url,
'name' => $final_name,
'size' => $file_size
]
];
}
// === УДАЛЕНИЕ ФАЙЛОВ ===
public static function delete($entity_type, $entity_id, $file_names) {
// Получаем конфигурацию
$config = self::getConfig($entity_type);
if (!$config) {
return ['success' => false, 'errors' => ['entity' => 'Неизвестный тип сущности']];
}
// Проверяем что сущность существует
$entity = self::getEntityData($config, $entity_id);
if (!$entity) {
return ['success' => false, 'errors' => ['entity' => 'Сущность не найдена']];
}
// Приводим к массиву если передана строка
if (!is_array($file_names)) {
$file_names = [$file_names];
}
// Получаем текущие файлы
$current_files = $entity[$config['field']] ? json_decode($entity[$config['field']], true) : [];
$upload_dir = self::$base_path . $config['folder'] . '/' . $entity_id;
$deleted = [];
// Удаляем каждый файл
foreach ($file_names as $file_name) {
foreach ($current_files as $index => $file) {
if ($file['name'] === $file_name) {
// Удаляем файл с диска
$file_path = $upload_dir . '/' . $file_name;
if (file_exists($file_path)) {
unlink($file_path);
}
// Удаляем из массива
array_splice($current_files, $index, 1);
$deleted[] = $file_name;
break;
}
}
}
// Обновляем в базе
self::updateEntityFiles($config, $entity_id, $current_files);
// Удаляем папку если она пустая
if (is_dir($upload_dir) && count(scandir($upload_dir)) === 2) {
rmdir($upload_dir);
}
return ['success' => true, 'deleted' => $deleted];
}
// === УДАЛЕНИЕ ПАПКИ СУЩНОСТИ (при удалении самой сущности) ===
public static function deleteFolder($entity_type, $entity_id) {
// Получаем конфигурацию
$config = self::getConfig($entity_type);
if (!$config) {
return false;
}
$upload_dir = self::$base_path . $config['folder'] . '/' . $entity_id;
if (is_dir($upload_dir)) {
// Удаляем все файлы в папке
$files = glob($upload_dir . '/*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
// Удаляем папку
rmdir($upload_dir);
}
return true;
}
}
?>