397 lines
16 KiB
PHP
397 lines
16 KiB
PHP
<?php
|
||
|
||
session_start();
|
||
require_once __DIR__ . '/config.php';
|
||
require_once __DIR__ . '/functions.php';
|
||
|
||
if (!file_exists(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
|
||
if (!file_exists(CHUNK_DIR)) mkdir(CHUNK_DIR, 0755, true);
|
||
|
||
// Проверка авторизации
|
||
if (!isset($_SESSION['logged_in']) || ($_GET['action'] ?? '') === 'logout') {
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$password = $_POST['password'] ?? '';
|
||
$csrf_token = $_POST['csrf_token'] ?? '';
|
||
|
||
// Проверяем CSRF токен
|
||
if (!verifyCSRFToken($csrf_token)) {
|
||
$error = 'Ошибка безопасности. Попробуйте еще раз.';
|
||
} elseif (password_verify($password, PASSWORD_HASH)) {
|
||
$_SESSION['logged_in'] = true;
|
||
header("Location: index.php");
|
||
exit;
|
||
} else {
|
||
$error = 'Неверный пароль';
|
||
}
|
||
}
|
||
|
||
$csrf_token = generateCSRFToken();
|
||
|
||
echo '<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Вход - Файловое хранилище</title>
|
||
<link rel="stylesheet" href="style.css">
|
||
</head>
|
||
<body>
|
||
<div class="login-form">
|
||
<h3>Вход в систему</h3>
|
||
' . (isset($error) ? '<div class="notification error" style="display: block;">' . htmlspecialchars($error) . '</div>' : '') . '
|
||
<form method="post">
|
||
<input type="hidden" name="csrf_token" value="' . htmlspecialchars($csrf_token) . '">
|
||
<input type="password" name="password" placeholder="Пароль" required>
|
||
<button type="submit" class="btn btn-primary" style="width: 100%;">Войти</button>
|
||
</form>
|
||
</div>
|
||
</body>
|
||
</html>';
|
||
exit;
|
||
}
|
||
|
||
// Удаление файла
|
||
if (isset($_GET['delete']) && isset($_GET['csrf_token'])) {
|
||
if (verifyCSRFToken($_GET['csrf_token'])) {
|
||
$filename = basename($_GET['delete']);
|
||
$filepath = UPLOAD_DIR . $filename;
|
||
|
||
if (file_exists($filepath)) {
|
||
@unlink($filepath);
|
||
$message = 'Файл удален успешно';
|
||
} else {
|
||
$error = 'Файл не найден';
|
||
}
|
||
} else {
|
||
$error = 'Ошибка безопасности';
|
||
}
|
||
|
||
header("Location: index.php" . (isset($message) ? "?msg=" . urlencode($message) : (isset($error) ? "?err=" . urlencode($error) : "")));
|
||
exit;
|
||
}
|
||
|
||
// Показ сообщений
|
||
$message = $_GET['msg'] ?? null;
|
||
$error = $_GET['err'] ?? null;
|
||
|
||
$files = array_diff(scandir(UPLOAD_DIR), ['.', '..']);
|
||
$csrf_token = generateCSRFToken();
|
||
|
||
// Функция для форматирования размера файла уже в functions.php
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html>
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Файловое хранилище</title>
|
||
<link rel="stylesheet" href="style.css">
|
||
</head>
|
||
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>Файловое хранилище</h1>
|
||
<a href="?action=logout" class="logout">Выйти</a>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<?php if ($message): ?>
|
||
<div id="server-message" data-type="success" data-message="<?= htmlspecialchars($message) ?>"></div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($error): ?>
|
||
<div id="server-message" data-type="error" data-message="<?= htmlspecialchars($error) ?>"></div>
|
||
<?php endif; ?>
|
||
|
||
<div id="notification" class="notification"></div>
|
||
|
||
<div class="upload-area">
|
||
<div class="upload-zone" onclick="document.getElementById('fileInput').click()">
|
||
<input type="file" id="fileInput" class="file-input-hidden">
|
||
<div class="upload-text">
|
||
<strong>Нажмите для выбора файла</strong> или перетащите файл сюда
|
||
</div>
|
||
</div>
|
||
|
||
<div class="progress-container" id="progressContainer">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" id="progressFill"></div>
|
||
</div>
|
||
<div class="progress-text" id="progressText"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (empty($files)): ?>
|
||
<div class="empty-state">
|
||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||
</svg>
|
||
<p>Файлы не загружены</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<ul class="files-list">
|
||
<?php foreach ($files as $file):
|
||
$filepath = UPLOAD_DIR . $file;
|
||
$filesize = file_exists($filepath) ? filesize($filepath) : 0;
|
||
$url = 'upload/' . rawurlencode($file);
|
||
$delete_url = '?delete=' . urlencode($file) . '&csrf_token=' . urlencode($csrf_token);
|
||
?>
|
||
<li class="file-item">
|
||
<div class="file-info">
|
||
<div class="file-name"><?= htmlspecialchars($file) ?></div>
|
||
<div class="file-size"><?= formatFileSize($filesize) ?></div>
|
||
</div>
|
||
<div class="file-actions">
|
||
<a href="<?= $url ?>" target="_blank" class="btn btn-secondary btn-small">Скачать</a>
|
||
<button onclick="copyLink('<?= htmlspecialchars($url) ?>')" class="btn btn-secondary btn-small">Копировать</button>
|
||
<a href="<?= $delete_url ?>" onclick="return confirm('Удалить файл <?= htmlspecialchars($file) ?>?')" class="btn btn-danger btn-small">Удалить</a>
|
||
</div>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const MAX_FILE_SIZE = <?= getMaxFileSize() ?>;
|
||
const CHUNK_SIZE = <?= getChunkSize() ?>;
|
||
const ALLOWED_EXTENSIONS = <?= json_encode(ALLOWED_EXTENSIONS) ?>;
|
||
const ALLOW_ALL_EXTENSIONS = ALLOWED_EXTENSIONS.length === 0 || ALLOWED_EXTENSIONS.includes('*');
|
||
const CSRF_TOKEN = '<?= htmlspecialchars($csrf_token) ?>';
|
||
|
||
// Показать серверные сообщения при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const serverMessage = document.getElementById('server-message');
|
||
if (serverMessage) {
|
||
const type = serverMessage.getAttribute('data-type');
|
||
const message = serverMessage.getAttribute('data-message');
|
||
showNotification(message, type);
|
||
serverMessage.remove();
|
||
}
|
||
|
||
// Drag & Drop функциональность
|
||
const uploadZone = document.querySelector('.upload-zone');
|
||
|
||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||
uploadZone.addEventListener(eventName, preventDefaults, false);
|
||
});
|
||
|
||
function preventDefaults(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
|
||
['dragenter', 'dragover'].forEach(eventName => {
|
||
uploadZone.addEventListener(eventName, highlight, false);
|
||
});
|
||
|
||
['dragleave', 'drop'].forEach(eventName => {
|
||
uploadZone.addEventListener(eventName, unhighlight, false);
|
||
});
|
||
|
||
function highlight(e) {
|
||
uploadZone.classList.add('dragover');
|
||
}
|
||
|
||
function unhighlight(e) {
|
||
uploadZone.classList.remove('dragover');
|
||
}
|
||
|
||
uploadZone.addEventListener('drop', handleDrop, false);
|
||
|
||
function handleDrop(e) {
|
||
const dt = e.dataTransfer;
|
||
const files = dt.files;
|
||
|
||
if (files.length > 0) {
|
||
document.getElementById('fileInput').files = files;
|
||
handleFileSelect(files[0]);
|
||
}
|
||
}
|
||
|
||
// Обработчик выбора файла через input
|
||
const input = document.getElementById('fileInput');
|
||
input.addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
console.log('Файл выбран:', file.name, 'Размер:', file.size);
|
||
handleFileSelect(file);
|
||
}
|
||
});
|
||
});
|
||
|
||
function showNotification(message, type = 'info') {
|
||
const notification = document.getElementById('notification');
|
||
notification.textContent = message;
|
||
notification.className = `notification ${type}`;
|
||
notification.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
notification.style.display = 'none';
|
||
}, 5000);
|
||
}
|
||
|
||
function copyLink(link) {
|
||
navigator.clipboard.writeText(window.location.origin + '/' + link).then(() => {
|
||
showNotification("Ссылка скопирована в буфер обмена", 'success');
|
||
}).catch(() => {
|
||
showNotification("Ошибка копирования ссылки", 'error');
|
||
});
|
||
}
|
||
|
||
function updateProgress(current, total) {
|
||
const progressContainer = document.getElementById('progressContainer');
|
||
const progressFill = document.getElementById('progressFill');
|
||
const progressText = document.getElementById('progressText');
|
||
|
||
if (current === 0) {
|
||
progressContainer.style.display = 'block';
|
||
}
|
||
|
||
const percentage = Math.round((current / total) * 100);
|
||
progressFill.style.width = percentage + '%';
|
||
progressText.textContent = `Загружено: ${current}/${total} (${percentage}%)`;
|
||
|
||
if (current === total) {
|
||
setTimeout(() => {
|
||
progressContainer.style.display = 'none';
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
function validateFile(file) {
|
||
console.log('Валидация файла:', file.name);
|
||
|
||
// Проверка размера
|
||
if (file.size > MAX_FILE_SIZE) {
|
||
const maxSizeMB = Math.round(MAX_FILE_SIZE / 1024 / 1024);
|
||
showNotification(`Файл слишком большой. Максимальный размер: ${maxSizeMB} MB`, 'error');
|
||
return false;
|
||
}
|
||
|
||
// Проверка расширения
|
||
if (!ALLOW_ALL_EXTENSIONS) {
|
||
const extension = file.name.split('.').pop().toLowerCase();
|
||
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
||
showNotification(`Недопустимый тип файла. Разрешены: ${ALLOWED_EXTENSIONS.join(', ')}`, 'error');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Проверка имени файла
|
||
if (!/^[a-zA-Z0-9а-яА-Я._\-\s()]+$/u.test(file.name)) {
|
||
showNotification('Имя файла содержит недопустимые символы', 'error');
|
||
return false;
|
||
}
|
||
|
||
if (file.name.length > 255) {
|
||
showNotification('Имя файла слишком длинное (максимум 255 символов)', 'error');
|
||
return false;
|
||
}
|
||
|
||
console.log('Файл прошел валидацию');
|
||
return true;
|
||
}
|
||
|
||
function handleFileSelect(file) {
|
||
console.log('Обработка выбранного файла:', file ? file.name : 'null');
|
||
|
||
if (!file) {
|
||
console.log('Файл не выбран');
|
||
return;
|
||
}
|
||
|
||
if (!validateFile(file)) {
|
||
console.log('Файл не прошел валидацию');
|
||
document.getElementById('fileInput').value = '';
|
||
return;
|
||
}
|
||
|
||
console.log('Начинаем загрузку файла');
|
||
uploadFile(file);
|
||
}
|
||
|
||
async function uploadFile(file) {
|
||
const chunkSize = CHUNK_SIZE;
|
||
const totalChunks = Math.ceil(file.size / chunkSize);
|
||
|
||
console.log(`Начинаем загрузку файла ${file.name}, размер: ${file.size} bytes, чанков: ${totalChunks}`);
|
||
|
||
showNotification('Начинаем загрузку файла...', 'info');
|
||
|
||
try {
|
||
// Сбрасываем прогресс
|
||
updateProgress(0, totalChunks);
|
||
|
||
for (let i = 0; i < totalChunks; i++) {
|
||
console.log(`Загружаем чанк ${i + 1} из ${totalChunks}`);
|
||
|
||
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
|
||
const formData = new FormData();
|
||
formData.append('chunk', chunk);
|
||
formData.append('filename', file.name);
|
||
formData.append('index', i);
|
||
formData.append('total', totalChunks);
|
||
formData.append('csrf_token', CSRF_TOKEN);
|
||
|
||
const response = await fetch('upload_chunk.php', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
console.log(`Ответ сервера для чанка ${i + 1}:`, response.status, response.statusText);
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error(`Ошибка загрузки чанка ${i + 1}:`, errorText);
|
||
throw new Error(`Ошибка загрузки чанка ${i + 1}: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
updateProgress(i + 1, totalChunks);
|
||
}
|
||
|
||
console.log('Все чанки загружены, объединяем файл');
|
||
showNotification('Объединяем файл...', 'info');
|
||
|
||
const mergeResponse = await fetch('merge_chunks.php', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
filename: file.name,
|
||
total: totalChunks,
|
||
csrf_token: CSRF_TOKEN
|
||
})
|
||
});
|
||
|
||
console.log('Ответ сервера при объединении:', mergeResponse.status, mergeResponse.statusText);
|
||
|
||
if (!mergeResponse.ok) {
|
||
const errorText = await mergeResponse.text();
|
||
console.error('Ошибка при объединении файла:', errorText);
|
||
throw new Error(`Ошибка при объединении файла: ${mergeResponse.status} ${mergeResponse.statusText}`);
|
||
}
|
||
|
||
console.log('Файл успешно загружен!');
|
||
showNotification('Файл успешно загружен!', 'success');
|
||
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 1500);
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки:', error);
|
||
showNotification('Ошибка загрузки: ' + error.message, 'error');
|
||
document.getElementById('fileInput').value = '';
|
||
document.getElementById('progressContainer').style.display = 'none';
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|