Добавлена загрузка rar, zip
1. Правка фронта 2. Правка бекенда
This commit is contained in:
@@ -128,7 +128,7 @@ class Task extends BaseEntity {
|
|||||||
self::check_task($id);
|
self::check_task($id);
|
||||||
|
|
||||||
// Удаляем папку с файлами если есть
|
// Удаляем папку с файлами если есть
|
||||||
$upload_dir = __DIR__ . '/../../../public/img/task/' . $id;
|
$upload_dir = __DIR__ . '/../../../public/task/' . $id;
|
||||||
if (is_dir($upload_dir)) {
|
if (is_dir($upload_dir)) {
|
||||||
$files = glob($upload_dir . '/*');
|
$files = glob($upload_dir . '/*');
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
|
|||||||
@@ -3,24 +3,24 @@
|
|||||||
class TaskImage {
|
class TaskImage {
|
||||||
|
|
||||||
protected static $db_name = 'cards_task';
|
protected static $db_name = 'cards_task';
|
||||||
protected static $upload_path = '/public/img/task/';
|
protected static $upload_path = '/public/task/';
|
||||||
|
|
||||||
// Валидация всех данных для загрузки
|
// Валидация всех данных для загрузки
|
||||||
protected static function validate($task_id, $file_base64, $file_name) {
|
protected static function validate($task_id, $file_base64, $file_name) {
|
||||||
// Проверка и получение задачи
|
// Проверка и получение задачи
|
||||||
$task = Task::check_task($task_id);
|
$task = Task::check_task($task_id);
|
||||||
|
|
||||||
// Декодируем base64
|
// Декодируем base64 (убираем любой data: префикс)
|
||||||
$file_data = base64_decode(preg_replace('/^data:image\/\w+;base64,/', '', $file_base64));
|
$file_data = base64_decode(preg_replace('/^data:[^;]+;base64,/', '', $file_base64));
|
||||||
if (!$file_data) {
|
if (!$file_data) {
|
||||||
return ['success' => false, 'errors' => ['file' => 'Ошибка декодирования файла']];
|
return ['success' => false, 'errors' => ['file' => 'Ошибка декодирования файла']];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка расширения
|
// Проверка расширения
|
||||||
$allowed_ext = ['png', 'jpg', 'jpeg'];
|
$allowed_ext = ['png', 'jpg', 'jpeg', 'zip', 'rar'];
|
||||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||||
if (!in_array($ext, $allowed_ext)) {
|
if (!in_array($ext, $allowed_ext)) {
|
||||||
return ['success' => false, 'errors' => ['file' => 'Разрешены только PNG, JPG, JPEG']];
|
return ['success' => false, 'errors' => ['file' => 'Разрешены только PNG, JPG, JPEG, ZIP, RAR']];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка размера (10 МБ)
|
// Проверка размера (10 МБ)
|
||||||
@@ -29,18 +29,11 @@ class TaskImage {
|
|||||||
return ['success' => false, 'errors' => ['file' => 'Файл слишком большой. Максимум 10 МБ']];
|
return ['success' => false, 'errors' => ['file' => 'Файл слишком большой. Максимум 10 МБ']];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка MIME типа
|
|
||||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
|
||||||
$mime = $finfo->buffer($file_data);
|
|
||||||
$allowed_mime = ['image/png', 'image/jpeg'];
|
|
||||||
if (!in_array($mime, $allowed_mime)) {
|
|
||||||
return ['success' => false, 'errors' => ['file' => 'Недопустимый тип файла']];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Всё ок — возвращаем данные
|
// Всё ок — возвращаем данные
|
||||||
return [
|
return [
|
||||||
'task' => $task,
|
'task' => $task,
|
||||||
'file_data' => $file_data
|
'file_data' => $file_data,
|
||||||
|
'is_archive' => in_array($ext, ['zip', 'rar'])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +62,7 @@ class TaskImage {
|
|||||||
$file_data = $validation['file_data'];
|
$file_data = $validation['file_data'];
|
||||||
|
|
||||||
// Путь к папке
|
// Путь к папке
|
||||||
$upload_dir = __DIR__ . '/../../../public/img/task/' . $task_id;
|
$upload_dir = __DIR__ . '/../../../public/task/' . $task_id;
|
||||||
if (!is_dir($upload_dir)) {
|
if (!is_dir($upload_dir)) {
|
||||||
mkdir($upload_dir, 0755, true);
|
mkdir($upload_dir, 0755, true);
|
||||||
}
|
}
|
||||||
@@ -123,7 +116,7 @@ class TaskImage {
|
|||||||
|
|
||||||
// Получаем текущие файлы
|
// Получаем текущие файлы
|
||||||
$current_files = json_decode($task['file_img'], true) ?? [];
|
$current_files = json_decode($task['file_img'], true) ?? [];
|
||||||
$upload_dir = __DIR__ . '/../../../public/img/task/' . $task_id;
|
$upload_dir = __DIR__ . '/../../../public/task/' . $task_id;
|
||||||
$deleted = [];
|
$deleted = [];
|
||||||
|
|
||||||
// Удаляем каждый файл
|
// Удаляем каждый файл
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Игнорирование статических файлов
|
// Обработка статических файлов
|
||||||
function ignore_favicon() {
|
function routing_static_files() {
|
||||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
if (preg_match('/\.(ico|png|jpg|jpeg|gif|css|js|svg|woff|woff2|ttf|eot)$/i', $requestUri)) {
|
$path = parse_url($requestUri, PHP_URL_PATH);
|
||||||
|
|
||||||
|
// Отдача файлов из /public/ (принудительное скачивание)
|
||||||
|
if (strpos($path, '/public/') === 0) {
|
||||||
|
$file = dirname(dirname(__DIR__)) . $path;
|
||||||
|
if (is_file($file)) {
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Content-Length: ' . filesize($file));
|
||||||
|
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
|
||||||
|
readfile($file);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
http_response_code(404);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Игнорирование favicon и прочих статических запросов
|
||||||
|
if (preg_match('/\.(ico|css|js|woff|woff2|ttf|eot)$/i', $requestUri)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
ignore_favicon();
|
routing_static_files();
|
||||||
|
|
||||||
check_ApiAuth($publicActions);
|
check_ApiAuth($publicActions);
|
||||||
handleRouting($routes);
|
handleRouting($routes);
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
accept=".png,.jpg,.jpeg,image/png,image/jpeg"
|
accept=".png,.jpg,.jpeg,.zip,.rar,image/png,image/jpeg,application/zip,application/x-rar-compressed"
|
||||||
multiple
|
multiple
|
||||||
@change="handleFileSelect"
|
@change="handleFileSelect"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
@@ -189,8 +189,14 @@
|
|||||||
<i data-lucide="x"></i>
|
<i data-lucide="x"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-thumbnail" @click="openImagePreview(file)">
|
<div class="file-thumbnail" @click="!isArchive(file) && openImagePreview(file)">
|
||||||
<img :src="getFullUrl(file.preview)" :alt="file.name">
|
<template v-if="isArchive(file)">
|
||||||
|
<div class="archive-icon">
|
||||||
|
<i data-lucide="archive"></i>
|
||||||
|
<span class="archive-ext">.{{ file.name.split('.').pop() }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<img v-else :src="getFullUrl(file.preview)" :alt="file.name">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -274,7 +280,13 @@
|
|||||||
<i data-lucide="x"></i>
|
<i data-lucide="x"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<img :src="getFullUrl(previewImage.preview)" :alt="previewImage.name" class="image-preview-full">
|
<template v-if="isArchive(previewImage)">
|
||||||
|
<div class="archive-preview-icon">
|
||||||
|
<i data-lucide="archive"></i>
|
||||||
|
<span>{{ previewImage.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<img v-else :src="getFullUrl(previewImage.preview)" :alt="previewImage.name" class="image-preview-full">
|
||||||
<div class="image-preview-info">
|
<div class="image-preview-info">
|
||||||
<span class="preview-name">{{ previewImage.name }}</span>
|
<span class="preview-name">{{ previewImage.name }}</span>
|
||||||
<span class="preview-size">{{ formatFileSize(previewImage.size) }}</span>
|
<span class="preview-size">{{ formatFileSize(previewImage.size) }}</span>
|
||||||
@@ -335,9 +347,25 @@ const userSearch = ref('')
|
|||||||
const attachedFiles = ref([])
|
const attachedFiles = ref([])
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const fileError = ref('')
|
const fileError = ref('')
|
||||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg']
|
const allowedExtensions = ['png', 'jpg', 'jpeg', 'zip', 'rar']
|
||||||
|
const archiveExtensions = ['zip', 'rar']
|
||||||
const maxFileSize = 10 * 1024 * 1024 // 10 MB
|
const maxFileSize = 10 * 1024 * 1024 // 10 MB
|
||||||
|
|
||||||
|
// Получить расширение файла
|
||||||
|
const getFileExt = (file) => {
|
||||||
|
return file.name?.split('.').pop()?.toLowerCase() || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка разрешён ли файл (по расширению)
|
||||||
|
const isAllowedFile = (file) => {
|
||||||
|
return allowedExtensions.includes(getFileExt(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка является ли файл архивом
|
||||||
|
const isArchive = (file) => {
|
||||||
|
return archiveExtensions.includes(getFileExt(file))
|
||||||
|
}
|
||||||
|
|
||||||
// Видимые файлы (без помеченных на удаление)
|
// Видимые файлы (без помеченных на удаление)
|
||||||
const visibleFiles = computed(() => attachedFiles.value.filter(f => !f.toDelete))
|
const visibleFiles = computed(() => attachedFiles.value.filter(f => !f.toDelete))
|
||||||
|
|
||||||
@@ -675,8 +703,8 @@ const processFiles = (files) => {
|
|||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
// Проверка типа файла
|
// Проверка типа файла
|
||||||
if (!allowedTypes.includes(file.type)) {
|
if (!isAllowedFile(file)) {
|
||||||
fileError.value = `Файл "${file.name}" не поддерживается. Разрешены только PNG, JPEG, JPG.`
|
fileError.value = `Файл "${file.name}" не поддерживается. Разрешены: PNG, JPEG, JPG, ZIP, RAR.`
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1422,6 +1450,34 @@ onUpdated(refreshIcons)
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archive-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-icon i {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-ext {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.file-preview-item:hover .file-thumbnail {
|
.file-preview-item:hover .file-thumbnail {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
@@ -1603,6 +1659,29 @@ onUpdated(refreshIcons)
|
|||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archive-preview-icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 60px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-preview-icon i {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-preview-icon span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.image-preview-info {
|
.image-preview-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user