1
0

Добавлена загрузка rar, zip

1. Правка фронта
2. Правка бекенда
This commit is contained in:
2026-01-12 07:01:31 +07:00
parent 61c336b703
commit b4263bfe3f
5 changed files with 117 additions and 28 deletions

View File

@@ -128,7 +128,7 @@ class Task extends BaseEntity {
self::check_task($id);
// Удаляем папку с файлами если есть
$upload_dir = __DIR__ . '/../../../public/img/task/' . $id;
$upload_dir = __DIR__ . '/../../../public/task/' . $id;
if (is_dir($upload_dir)) {
$files = glob($upload_dir . '/*');
foreach ($files as $file) {

View File

@@ -3,24 +3,24 @@
class TaskImage {
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) {
// Проверка и получение задачи
$task = Task::check_task($task_id);
// Декодируем base64
$file_data = base64_decode(preg_replace('/^data:image\/\w+;base64,/', '', $file_base64));
// Декодируем base64 (убираем любой data: префикс)
$file_data = base64_decode(preg_replace('/^data:[^;]+;base64,/', '', $file_base64));
if (!$file_data) {
return ['success' => false, 'errors' => ['file' => 'Ошибка декодирования файла']];
}
// Проверка расширения
$allowed_ext = ['png', 'jpg', 'jpeg'];
$allowed_ext = ['png', 'jpg', 'jpeg', 'zip', 'rar'];
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
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 МБ)
@@ -29,18 +29,11 @@ class TaskImage {
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 [
'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'];
// Путь к папке
$upload_dir = __DIR__ . '/../../../public/img/task/' . $task_id;
$upload_dir = __DIR__ . '/../../../public/task/' . $task_id;
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
@@ -123,7 +116,7 @@ class TaskImage {
// Получаем текущие файлы
$current_files = json_decode($task['file_img'], true) ?? [];
$upload_dir = __DIR__ . '/../../../public/img/task/' . $task_id;
$upload_dir = __DIR__ . '/../../../public/task/' . $task_id;
$deleted = [];
// Удаляем каждый файл

View File

@@ -1,9 +1,26 @@
<?php
// Игнорирование статических файлов
function ignore_favicon() {
// Обработка статических файлов
function routing_static_files() {
$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);
exit;
}

View File

@@ -15,7 +15,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
ignore_favicon();
routing_static_files();
check_ApiAuth($publicActions);
handleRouting($routes);

View File

@@ -144,7 +144,7 @@
<input
type="file"
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
@change="handleFileSelect"
style="display: none"
@@ -189,8 +189,14 @@
<i data-lucide="x"></i>
</button>
</div>
<div class="file-thumbnail" @click="openImagePreview(file)">
<img :src="getFullUrl(file.preview)" :alt="file.name">
<div class="file-thumbnail" @click="!isArchive(file) && openImagePreview(file)">
<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>
@@ -274,7 +280,13 @@
<i data-lucide="x"></i>
</button>
</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">
<span class="preview-name">{{ previewImage.name }}</span>
<span class="preview-size">{{ formatFileSize(previewImage.size) }}</span>
@@ -335,9 +347,25 @@ const userSearch = ref('')
const attachedFiles = ref([])
const isDragging = ref(false)
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 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))
@@ -675,8 +703,8 @@ const processFiles = (files) => {
for (const file of files) {
// Проверка типа файла
if (!allowedTypes.includes(file.type)) {
fileError.value = `Файл "${file.name}" не поддерживается. Разрешены только PNG, JPEG, JPG.`
if (!isAllowedFile(file)) {
fileError.value = `Файл "${file.name}" не поддерживается. Разрешены: PNG, JPEG, JPG, ZIP, RAR.`
continue
}
@@ -1422,6 +1450,34 @@ onUpdated(refreshIcons)
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 {
opacity: 0.85;
}
@@ -1603,6 +1659,29 @@ onUpdated(refreshIcons)
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 {
display: flex;
align-items: center;