Загрузка файлов
Полное руководство по загрузке, обработке и управлению файлами в MNRFY
Введение
MNRFY предоставляет мощные инструменты для работы с загружаемыми файлами: изображения, документы, архивы и другие типы. Система поддерживает валидацию, обработку изображений, организацию файлов и защиту от вредоносных файлов.
Базовая загрузка файлов
Простая форма загрузки
<form method="POST" action="/" enctype="multipart/form-data">
{{ csrf() }}
<input type="hidden" name="__handler" value="upload-file">
<div class="form-group">
<label>Выберите файл</label>
<input type="file" name="file" required>
<small class="form-text text-muted">
Максимальный размер: 10 МБ. Разрешены: JPG, PNG, PDF, DOC
</small>
</div>
<div class="form-group">
<label>Описание файла</label>
<input type="text" name="description" maxlength="255">
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload"></i> Загрузить файл
</button>
</form>
Базовый обработчик загрузки
<?php
// /src/handlers/upload-file.php
return function($request, $response, $context) {
$userId = $_SESSION['user_id'] ?? null;
if (!$userId) {
return [
'success' => false,
'error' => 'Необходима авторизация'
];
}
$file = $request->file('file');
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
return [
'success' => false,
'error' => 'Ошибка загрузки файла'
];
}
// Валидация типа файла
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/msword'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
return [
'success' => false,
'error' => 'Недопустимый тип файла'
];
}
// Валидация размера (10 МБ)
if ($file['size'] > 10 * 1024 * 1024) {
return [
'success' => false,
'error' => 'Файл слишком большой (максимум 10 МБ)'
];
}
// Генерируем уникальное имя файла
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = 'file_' . $userId . '_' . time() . '_' . uniqid() . '.' . $extension;
$uploadDir = MNRFY_UPLOADS . '/documents/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$uploadPath = $uploadDir . $filename;
if (move_uploaded_file($file['tmp_name'], $uploadPath)) {
// Сохраняем информацию о файле в БД
$db = $context->db('1752665380840');
$fileId = $db->addItem('uploaded_files', [
'user_id' => $userId,
'original_name' => $file['name'],
'filename' => $filename,
'mime_type' => $mimeType,
'size' => $file['size'],
'path' => '/documents/' . $filename,
'description' => $request->input('description', ''),
'created_at' => time(),
'ip_address' => $request->ip()
]);
return [
'success' => true,
'message' => 'Файл успешно загружен',
'data' => [
'file_id' => $fileId,
'filename' => $filename,
'url' => '/mnrfy-uploads/documents/' . $filename
]
];
}
return [
'success' => false,
'error' => 'Не удалось сохранить файл'
];
};
?>
Загрузка изображений
Форма загрузки аватара
<form method="POST" action="/" enctype="multipart/form-data" class="avatar-upload-form">
{{ csrf() }}
<input type="hidden" name="__handler" value="upload-avatar">
<div class="avatar-upload-container">
<div class="current-avatar">
{% if user.avatar %}
<img src="{{ user.avatar }}" alt="Текущий аватар" class="avatar-preview">
{% else %}
<div class="no-avatar">
<i class="fas fa-user fa-3x"></i>
<p>Нет аватара</p>
</div>
{% endif %}
</div>
<div class="upload-controls">
<input type="file" id="avatar-input" name="avatar" accept="image/*"
onchange="previewImage(this)" style="display: none;">
<label for="avatar-input" class="btn btn-primary">
<i class="fas fa-camera"></i> Выбрать фото
</label>
<div class="upload-requirements">
<h5>Требования к изображению:</h5>
<ul>
<li>Максимальный размер: 5 МБ</li>
<li>Форматы: JPG, PNG, GIF, WebP</li>
<li>Минимальные размеры: 100x100 пикселей</li>
<li>Рекомендуемые размеры: 400x400 пикселей</li>
</ul>
</div>
</div>
</div>
<div class="image-preview" id="imagePreview" style="display: none;">
<h5>Предварительный просмотр:</h5>
<img id="previewImg" style="max-width: 300px; max-height: 300px; border-radius: 8px;">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-success" disabled id="upload-btn">
<i class="fas fa-upload"></i> Загрузить аватар
</button>
{% if user.avatar %}
<button type="button" class="btn btn-danger" onclick="removeAvatar()">
<i class="fas fa-trash"></i> Удалить аватар
</button>
{% endif %}
</div>
</form>
<script>
function previewImage(input) {
const file = input.files[0];
const preview = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
const uploadBtn = document.getElementById('upload-btn');
if (file) {
// Проверяем размер файла
if (file.size > 5 * 1024 * 1024) {
alert('Файл слишком большой! Максимальный размер: 5 МБ');
input.value = '';
return;
}
// Проверяем тип файла
if (!['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(file.type)) {
alert('Недопустимый тип файла! Разрешены: JPG, PNG, GIF, WebP');
input.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
previewImg.src = e.target.result;
preview.style.display = 'block';
uploadBtn.disabled = false;
// Проверяем размеры изображения
previewImg.onload = function() {
if (this.naturalWidth < 100 || this.naturalHeight < 100) {
alert('Изображение слишком маленькое! Минимальные размеры: 100x100 пикселей');
input.value = '';
preview.style.display = 'none';
uploadBtn.disabled = true;
}
};
};
reader.readAsDataURL(file);
} else {
preview.style.display = 'none';
uploadBtn.disabled = true;
}
}
function removeAvatar() {
if (confirm('Вы уверены, что хотите удалить аватар?')) {
fetch('/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
'__handler': 'remove-avatar',
'_token': document.querySelector('[name="_token"]').value
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.error || 'Ошибка при удалении аватара');
}
});
}
}
</script>
Обработчик загрузки аватара
<?php
// /src/handlers/upload-avatar.php
return function($request, $response, $context) {
$userId = $_SESSION['user_id'] ?? null;
if (!$userId) {
return ['success' => false, 'error' => 'Необходима авторизация'];
}
$file = $request->file('avatar');
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => 'Ошибка загрузки файла'];
}
// Валидация типа файла
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
return ['success' => false, 'error' => 'Недопустимый тип файла'];
}
// Валидация размера файла (5MB)
if ($file['size'] > 5 * 1024 * 1024) {
return ['success' => false, 'error' => 'Файл слишком большой (максимум 5MB)'];
}
// Валидация размеров изображения
$imageInfo = getimagesize($file['tmp_name']);
if (!$imageInfo) {
return ['success' => false, 'error' => 'Неверный формат изображения'];
}
[$width, $height] = $imageInfo;
if ($width < 100 || $height < 100) {
return ['success' => false, 'error' => 'Изображение слишком маленькое (минимум 100x100px)'];
}
if ($width > 4000 || $height > 4000) {
return ['success' => false, 'error' => 'Изображение слишком большое (максимум 4000x4000px)'];
}
try {
$db = $context->db('1752665380840');
// Получаем текущего пользователя
$user = $db->getItem('users', ['id' => $userId]);
// Генерируем уникальное имя файла
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$filename = 'avatar_' . $userId . '_' . time() . '_' . uniqid() . '.' . $extension;
$uploadDir = MNRFY_UPLOADS . '/avatars/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$uploadPath = $uploadDir . $filename;
if (move_uploaded_file($file['tmp_name'], $uploadPath)) {
// Создаем миниатюры разных размеров
$thumbnails = $this->createThumbnails($uploadPath, $uploadDir, $filename, [
'thumb' => [150, 150],
'medium' => [300, 300],
'large' => [600, 600]
]);
// Удаляем старый аватар
if ($user['avatar']) {
$this->removeOldAvatar($user['avatar']);
}
// Обновляем в базе данных
$db->editItem('users', ['id' => $userId], [
'avatar' => '/avatars/' . $filename,
'avatar_thumbnails' => json_encode($thumbnails),
'updated_at' => time()
]);
// Записываем в логи
$db->addItem('user_logs', [
'user_id' => $userId,
'action' => 'avatar_updated',
'details' => json_encode([
'filename' => $filename,
'original_name' => $file['name'],
'size' => $file['size'],
'dimensions' => $width . 'x' . $height
]),
'ip_address' => $request->ip(),
'created_at' => time()
]);
return [
'success' => true,
'message' => 'Аватар успешно загружен',
'data' => [
'avatar_url' => '/mnrfy-uploads/avatars/' . $filename,
'thumbnails' => $thumbnails
]
];
}
return ['success' => false, 'error' => 'Не удалось сохранить файл'];
} catch (Exception $e) {
error_log('Avatar upload error: ' . $e->getMessage());
return [
'success' => false,
'error' => 'Произошла ошибка при загрузке аватара'
];
}
};
// Функция создания миниатюр
function createThumbnails($sourcePath, $uploadDir, $filename, $sizes) {
$thumbnails = [];
$imageInfo = getimagesize($sourcePath);
if (!$imageInfo) return $thumbnails;
[$sourceWidth, $sourceHeight, $sourceType] = $imageInfo;
// Создаем изображение из источника
switch ($sourceType) {
case IMAGETYPE_JPEG:
$sourceImage = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$sourceImage = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$sourceImage = imagecreatefromgif($sourcePath);
break;
case IMAGETYPE_WEBP:
$sourceImage = imagecreatefromwebp($sourcePath);
break;
default:
return $thumbnails;
}
foreach ($sizes as $sizeName => [$thumbWidth, $thumbHeight]) {
// Вычисляем размеры с сохранением пропорций
$sourceAspectRatio = $sourceWidth / $sourceHeight;
$thumbAspectRatio = $thumbWidth / $thumbHeight;
if ($sourceAspectRatio > $thumbAspectRatio) {
// Обрезаем по ширине
$newHeight = $sourceHeight;
$newWidth = $sourceHeight * $thumbAspectRatio;
$cropX = ($sourceWidth - $newWidth) / 2;
$cropY = 0;
} else {
// Обрезаем по высоте
$newWidth = $sourceWidth;
$newHeight = $sourceWidth / $thumbAspectRatio;
$cropX = 0;
$cropY = ($sourceHeight - $newHeight) / 2;
}
// Создаем миниатюру
$thumbImage = imagecreatetruecolor($thumbWidth, $thumbHeight);
// Сохраняем прозрачность для PNG
if ($sourceType == IMAGETYPE_PNG) {
imagecolortransparent($thumbImage, imagecolorallocatealpha($thumbImage, 0, 0, 0, 127));
imagealphablending($thumbImage, false);
imagesavealpha($thumbImage, true);
}
imagecopyresampled(
$thumbImage, $sourceImage,
0, 0, $cropX, $cropY,
$thumbWidth, $thumbHeight, $newWidth, $newHeight
);
// Сохраняем миниатюру
$thumbFilename = $sizeName . '_' . $filename;
$thumbPath = $uploadDir . $thumbFilename;
switch ($sourceType) {
case IMAGETYPE_JPEG:
imagejpeg($thumbImage, $thumbPath, 85);
break;
case IMAGETYPE_PNG:
imagepng($thumbImage, $thumbPath, 8);
break;
case IMAGETYPE_GIF:
imagegif($thumbImage, $thumbPath);
break;
case IMAGETYPE_WEBP:
imagewebp($thumbImage, $thumbPath, 85);
break;
}
imagedestroy($thumbImage);
$thumbnails[$sizeName] = '/avatars/' . $thumbFilename;
}
imagedestroy($sourceImage);
return $thumbnails;
}
// Функция удаления старого аватара
function removeOldAvatar($avatarPath) {
$fullPath = MNRFY_UPLOADS . ltrim($avatarPath, '/');
if (file_exists($fullPath)) {
unlink($fullPath);
// Удаляем миниатюры
$dir = dirname($fullPath);
$filename = basename($fullPath);
$thumbnailPrefixes = ['thumb_', 'medium_', 'large_'];
foreach ($thumbnailPrefixes as $prefix) {
$thumbPath = $dir . '/' . $prefix . $filename;
if (file_exists($thumbPath)) {
unlink($thumbPath);
}
}
}
}
?>
Множественная загрузка файлов
Форма для загрузки нескольких файлов
<form method="POST" action="/" enctype="multipart/form-data" class="multiple-upload-form">
{{ csrf() }}
<input type="hidden" name="__handler" value="upload-gallery">
<div class="upload-zone" id="uploadZone">
<div class="upload-message">
<i class="fas fa-cloud-upload-alt fa-3x"></i>
<h4>Перетащите файлы сюда или нажмите для выбора</h4>
<p>Можно загружать несколько изображений одновременно</p>
<input type="file" id="fileInput" name="files[]" multiple accept="image/*" style="display: none;">
</div>
</div>
<div id="filePreview" class="file-preview-container" style="display: none;">
<h5>Выбранные файлы:</h5>
<div id="previewList" class="preview-list"></div>
<div class="upload-controls">
<button type="button" class="btn btn-secondary" onclick="clearFiles()">
<i class="fas fa-times"></i> Очистить
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload"></i> Загрузить файлы
</button>
</div>
</div>
<div class="progress" id="uploadProgress" style="display: none;">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
const filePreview = document.getElementById('filePreview');
const previewList = document.getElementById('previewList');
// Обработка клика по зоне загрузки
uploadZone.addEventListener('click', function() {
fileInput.click();
});
// Обработка drag & drop
uploadZone.addEventListener('dragover', function(e) {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', function(e) {
e.preventDefault();
uploadZone.classList.remove('dragover');
const files = e.dataTransfer.files;
handleFiles(files);
});
// Обработка выбора файлов
fileInput.addEventListener('change', function(e) {
handleFiles(e.target.files);
});
function handleFiles(files) {
previewList.innerHTML = '';
filePreview.style.display = 'block';
Array.from(files).forEach((file, index) => {
if (!file.type.startsWith('image/')) {
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const previewItem = document.createElement('div');
previewItem.className = 'preview-item';
previewItem.innerHTML = `
<img src="{$e.target.result}" alt="Preview" style="width: 120px; height: 120px; object-fit: cover;">
<div class="file-info">
<p class="file-name">{$file.name}</p>
<p class="file-size">{$(file.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
<button type="button" class="btn btn-sm btn-danger remove-file" onclick="removeFile(this, {$index})">
<i class="fas fa-times"></i>
</button>
`;
previewList.appendChild(previewItem);
};
reader.readAsDataURL(file);
});
}
// AJAX загрузка с прогресс-баром
document.querySelector('.multiple-upload-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const progressBar = document.querySelector('#uploadProgress .progress-bar');
const uploadProgress = document.getElementById('uploadProgress');
uploadProgress.style.display = 'block';
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
progressBar.textContent = Math.round(percentComplete) + '%';
}
});
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
alert('Файлы успешно загружены!');
location.reload();
} else {
alert(response.error || 'Ошибка при загрузке файлов');
}
} else {
alert('Ошибка сервера');
}
uploadProgress.style.display = 'none';
});
xhr.open('POST', '/');
xhr.send(formData);
});
});
function removeFile(button, index) {
button.closest('.preview-item').remove();
// Удаляем файл из input (это сложно, проще пересоздать список)
const fileInput = document.getElementById('fileInput');
const dt = new DataTransfer();
Array.from(fileInput.files).forEach((file, i) => {
if (i !== index) {
dt.items.add(file);
}
});
fileInput.files = dt.files;
if (fileInput.files.length === 0) {
clearFiles();
}
}
function clearFiles() {
document.getElementById('fileInput').value = '';
document.getElementById('filePreview').style.display = 'none';
document.getElementById('previewList').innerHTML = '';
}
</script>
Обработчик множественной загрузки
<?php
// /src/handlers/upload-gallery.php
return function($request, $response, $context) {
$userId = $_SESSION['user_id'] ?? null;
if (!$userId) {
return ['success' => false, 'error' => 'Необходима авторизация'];
}
$files = $request->file('files');
if (empty($files)) {
return ['success' => false, 'error' => 'Файлы не выбраны'];
}
$db = $context->db('1752665380840');
$uploadedFiles = [];
$errors = [];
$uploadDir = MNRFY_UPLOADS . '/gallery/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
foreach ($files as $index => $file) {
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = "Файл {$index}: ошибка загрузки";
continue;
}
// Валидация
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'])) {
$errors[] = "Файл {$file['name']}: недопустимый тип";
continue;
}
if ($file['size'] > 10 * 1024 * 1024) {
$errors[] = "Файл {$file['name']}: слишком большой размер";
continue;
}
$imageInfo = getimagesize($file['tmp_name']);
if (!$imageInfo) {
$errors[] = "Файл {$file['name']}: неверный формат изображения";
continue;
}
[$width, $height] = $imageInfo;
if ($width < 200 || $height < 200) {
$errors[] = "Файл {$file['name']}: слишком маленькое изображение";
continue;
}
// Загружаем файл
try {
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$filename = 'gallery_' . $userId . '_' . time() . '_' . uniqid() . '.' . $extension;
$uploadPath = $uploadDir . $filename;
if (move_uploaded_file($file['tmp_name'], $uploadPath)) {
// Создаем миниатюру
$thumbnailPath = $this->createGalleryThumbnail($uploadPath, $uploadDir, $filename, 300, 300);
// Оптимизируем оригинальное изображение
$this->optimizeImage($uploadPath, $mimeType, $width, $height);
// Сохраняем в БД
$fileId = $db->addItem('gallery_images', [
'user_id' => $userId,
'filename' => $filename,
'original_name' => $file['name'],
'mime_type' => $mimeType,
'size' => $file['size'],
'width' => $width,
'height' => $height,
'path' => '/gallery/' . $filename,
'thumbnail_path' => $thumbnailPath,
'status' => 'active',
'created_at' => time(),
'ip_address' => $request->ip()
]);
$uploadedFiles[] = [
'id' => $fileId,
'filename' => $filename,
'url' => '/mnrfy-uploads/gallery/' . $filename,
'thumbnail_url' => '/mnrfy-uploads' . $thumbnailPath
];
} else {
$errors[] = "Файл {$file['name']}: не удалось сохранить";
}
} catch (Exception $e) {
$errors[] = "Файл {$file['name']}: ошибка обработки";
error_log("Gallery upload error: " . $e->getMessage());
}
}
$result = [
'success' => !empty($uploadedFiles),
'uploaded_count' => count($uploadedFiles),
'uploaded_files' => $uploadedFiles
];
if (!empty($errors)) {
$result['errors'] = $errors;
$result['error_count'] = count($errors);
}
if (!empty($uploadedFiles)) {
$result['message'] = "Успешно загружено {count($uploadedFiles)} файлов";
if (!empty($errors)) {
$result['message'] .= " ({count($errors)} ошибок)";
}
} else {
$result['error'] = 'Не удалось загрузить ни одного файла';
}
return $result;
};
function createGalleryThumbnail($sourcePath, $uploadDir, $filename, $thumbWidth, $thumbHeight) {
$imageInfo = getimagesize($sourcePath);
if (!$imageInfo) return null;
[$sourceWidth, $sourceHeight, $sourceType] = $imageInfo;
// Создаем изображение из источника
switch ($sourceType) {
case IMAGETYPE_JPEG:
$sourceImage = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$sourceImage = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$sourceImage = imagecreatefromgif($sourcePath);
break;
case IMAGETYPE_WEBP:
$sourceImage = imagecreatefromwebp($sourcePath);
break;
default:
return null;
}
// Вычисляем размеры
$sourceAspectRatio = $sourceWidth / $sourceHeight;
$thumbAspectRatio = $thumbWidth / $thumbHeight;
if ($sourceAspectRatio > $thumbAspectRatio) {
$newHeight = $thumbHeight;
$newWidth = $thumbHeight * $sourceAspectRatio;
$offsetX = ($newWidth - $thumbWidth) / 2;
$offsetY = 0;
} else {
$newWidth = $thumbWidth;
$newHeight = $thumbWidth / $sourceAspectRatio;
$offsetX = 0;
$offsetY = ($newHeight - $thumbHeight) / 2;
}
// Создаем миниатюру
$thumbImage = imagecreatetruecolor($thumbWidth, $thumbHeight);
$tempImage = imagecreatetruecolor($newWidth, $newHeight);
// Масштабируем
imagecopyresampled($tempImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $sourceWidth, $sourceHeight);
// Обрезаем до нужного размера
imagecopy($thumbImage, $tempImage, 0, 0, $offsetX, $offsetY, $thumbWidth, $thumbHeight);
// Сохраняем
$thumbFilename = 'thumb_' . $filename;
$thumbPath = $uploadDir . $thumbFilename;
imagejpeg($thumbImage, $thumbPath, 85);
imagedestroy($sourceImage);
imagedestroy($tempImage);
imagedestroy($thumbImage);
return '/gallery/' . $thumbFilename;
}
function optimizeImage($imagePath, $mimeType, $width, $height) {
// Оптимизируем большие изображения
if ($width > 1920 || $height > 1920) {
$maxSize = 1920;
if ($width > $height) {
$newWidth = $maxSize;
$newHeight = ($height * $maxSize) / $width;
} else {
$newHeight = $maxSize;
$newWidth = ($width * $maxSize) / $height;
}
// Создаем оптимизированную версию
switch ($mimeType) {
case 'image/jpeg':
$sourceImage = imagecreatefromjpeg($imagePath);
$optimizedImage = imagecreatetruecolor($newWidth, $newHeight);
imagecopyresampled($optimizedImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
imagejpeg($optimizedImage, $imagePath, 85);
imagedestroy($sourceImage);
imagedestroy($optimizedImage);
break;
case 'image/png':
$sourceImage = imagecreatefrompng($imagePath);
$optimizedImage = imagecreatetruecolor($newWidth, $newHeight);
imagealphablending($optimizedImage, false);
imagesavealpha($optimizedImage, true);
imagecopyresampled($optimizedImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
imagepng($optimizedImage, $imagePath, 8);
imagedestroy($sourceImage);
imagedestroy($optimizedImage);
break;
}
}
}
?>
Управление файлами
Галерея загруженных файлов
<!-- Получаем файлы пользователя -->
{% set userFiles = db('1752665380840')->getItems('gallery_images', {
'user_id': auth_id(),
'status': 'active'
}, ['*'], 'created_at', 'DESC') %}
<div class="file-gallery">
<div class="gallery-header">
<h3>Ваши файлы ({{ userFiles | length }})</h3>
<button type="button" class="btn btn-primary" onclick="toggleUploadForm()">
<i class="fas fa-plus"></i> Добавить файлы
</button>
</div>
{% if userFiles | length > 0 %}
<div class="gallery-grid">
{% foreach userFiles as file %}
<div class="gallery-item" data-file-id="{{ file.id }}">
<div class="image-container">
<img src="{{ '/mnrfy-uploads' ~ file.thumbnail_path }}"
alt="{{ file.original_name }}"
onclick="openLightbox('{{ '/mnrfy-uploads' ~ file.path }}')">
<div class="image-overlay">
<div class="image-actions">
<button type="button" class="btn btn-sm btn-light"
onclick="copyImageUrl('{{ '/mnrfy-uploads' ~ file.path }}')"
title="Скопировать ссылку">
<i class="fas fa-link"></i>
</button>
<button type="button" class="btn btn-sm btn-light"
onclick="downloadImage('{{ '/mnrfy-uploads' ~ file.path }}', '{{ file.original_name }}')"
title="Скачать">
<i class="fas fa-download"></i>
</button>
<button type="button" class="btn btn-sm btn-danger"
onclick="deleteImage({{ file.id }})"
title="Удалить">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<div class="image-info">
<p class="image-name" title="{{ file.original_name }}">
{{ file.original_name | truncate(20) }}
</p>
<small class="image-meta">
{{ file.width }}x{{ file.height }} •
{{ (file.size / 1024) | round(1) }} КБ •
{{ file.created_at | date('d.m.Y') }}
</small>
</div>
</div>
{% endforeach %}
</div>
{% else %}
<div class="empty-gallery">
<i class="fas fa-images fa-4x"></i>
<h4>У вас пока нет загруженных файлов</h4>
<p>Нажмите кнопку "Добавить файлы" чтобы загрузить изображения</p>
</div>
{% endif %}
</div>
<!-- Lightbox для просмотра изображений -->
<div id="lightbox" class="lightbox" onclick="closeLightbox()">
<div class="lightbox-content">
<span class="lightbox-close" onclick="closeLightbox()">×</span>
<img id="lightboxImage" src="">
</div>
</div>
<script>
function copyImageUrl(url) {
const fullUrl = window.location.origin + url;
navigator.clipboard.writeText(fullUrl).then(() => {
showNotification('Ссылка скопирована в буфер обмена', 'success');
});
}
function downloadImage(url, filename) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
function deleteImage(fileId) {
if (confirm('Вы уверены, что хотите удалить это изображение?')) {
fetch('/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
'__handler': 'delete-image',
'file_id': fileId,
'_token': document.querySelector('[name="_token"]').value
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.querySelector(`[data-file-id="{$fileId}"]`).remove();
showNotification('Изображение удалено', 'success');
} else {
showNotification(data.error || 'Ошибка при удалении', 'error');
}
});
}
}
function openLightbox(imageUrl) {
document.getElementById('lightboxImage').src = imageUrl;
document.getElementById('lightbox').style.display = 'flex';
}
function closeLightbox() {
document.getElementById('lightbox').style.display = 'none';
}
function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification notification-{$type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
</script>
Отлично! Вы изучили все возможности работы с формами в MNRFY. Теперь переходите к изучению компонентов и макетов.