update ui

This commit is contained in:
2025-06-13 14:59:47 +03:00
parent 0311b21d23
commit 4de96eb632
7 changed files with 1503 additions and 93 deletions

627
index.php
View File

@@ -2,86 +2,571 @@
session_start();
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/functions.php';
if (!file_exists(UPLOAD_DIR)) mkdir(UPLOAD_DIR);
if (!file_exists(CHUNK_DIR)) mkdir(CHUNK_DIR);
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' && ($_POST['password'] ?? '') === PASSWORD) {
$_SESSION['logged_in'] = true;
header("Location: index.php");
// Обработка запросов файлов напрямую из корня
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$requestedFile = ltrim($requestUri, '/');
// Если запрашивается файл (не административный интерфейс)
if (
!empty($requestedFile) &&
$requestedFile !== 'index.php' &&
$requestedFile !== 'upload_chunk.php' &&
$requestedFile !== 'merge_chunks.php' &&
!isset($_GET['action']) &&
!isset($_GET['delete']) &&
!isset($_POST['password'])
) {
$filename = basename($requestedFile);
$filepath = UPLOAD_DIR . $filename;
if (file_exists($filepath)) {
// Определение MIME типа
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$mimeTypes = [
// Изображения
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
'bmp' => 'image/bmp',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'ico' => 'image/x-icon',
'heic' => 'image/heic',
'heif' => 'image/heif',
'avif' => 'image/avif',
// Документы
'pdf' => 'application/pdf',
'txt' => 'text/plain',
'rtf' => 'application/rtf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'odt' => 'application/vnd.oasis.opendocument.text',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'odp' => 'application/vnd.oasis.opendocument.presentation',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'epub' => 'application/epub+zip',
'mobi' => 'application/x-mobipocket-ebook',
// Архивы
'zip' => 'application/zip',
'rar' => 'application/vnd.rar',
'7z' => 'application/x-7z-compressed',
'tar' => 'application/x-tar',
'gz' => 'application/gzip',
'bz2' => 'application/x-bzip2',
'xz' => 'application/x-xz',
'iso' => 'application/x-iso9660-image',
// Видео
'mp4' => 'video/mp4',
'avi' => 'video/x-msvideo',
'mkv' => 'video/x-matroska',
'mov' => 'video/quicktime',
'webm' => 'video/webm',
'flv' => 'video/x-flv',
'wmv' => 'video/x-ms-wmv',
'mpg' => 'video/mpeg',
'mpeg' => 'video/mpeg',
'm4v' => 'video/x-m4v',
'3gp' => 'video/3gpp',
'ogv' => 'video/ogg',
// Аудио
'mp3' => 'audio/mpeg',
'wav' => 'audio/wav',
'flac' => 'audio/flac',
'ogg' => 'audio/ogg',
'aac' => 'audio/aac',
'm4a' => 'audio/mp4',
'wma' => 'audio/x-ms-wma',
'opus' => 'audio/opus',
'aiff' => 'audio/aiff',
// Данные
'json' => 'application/json',
'xml' => 'application/xml',
'csv' => 'text/csv',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'yaml' => 'text/yaml',
'yml' => 'text/yaml',
'toml' => 'text/toml',
// Код
'html' => 'text/html',
'htm' => 'text/html',
'css' => 'text/css',
'js' => 'text/javascript',
'php' => 'text/x-php',
'py' => 'text/x-python',
'java' => 'text/x-java-source',
'cpp' => 'text/x-c++src',
'c' => 'text/x-csrc',
'h' => 'text/x-chdr',
'cs' => 'text/x-csharp',
'rb' => 'text/x-ruby',
'go' => 'text/x-go',
'rs' => 'text/x-rust',
'sql' => 'text/x-sql',
'md' => 'text/markdown',
'log' => 'text/plain',
'sh' => 'text/x-shellscript',
'bat' => 'text/x-msdos-batch',
// Веб и CMS
'wpress' => 'application/octet-stream',
'backup' => 'application/octet-stream',
'bak' => 'application/octet-stream',
'db' => 'application/x-sqlite3',
'sqlite' => 'application/x-sqlite3',
'sqlite3' => 'application/x-sqlite3',
// Шрифты
'ttf' => 'font/ttf',
'otf' => 'font/otf',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'eot' => 'application/vnd.ms-fontobject',
// Дизайн
'psd' => 'image/vnd.adobe.photoshop',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'indd' => 'application/x-indesign',
// CAD
'dwg' => 'image/vnd.dwg',
'dxf' => 'image/vnd.dxf',
'obj' => 'model/obj',
'stl' => 'model/stl',
// Мобильные приложения и исполняемые файлы
'apk' => 'application/vnd.android.package-archive',
'ipa' => 'application/octet-stream',
'exe' => 'application/x-msdownload',
'msi' => 'application/x-msi',
'deb' => 'application/vnd.debian.binary-package',
'rpm' => 'application/x-rpm',
'dmg' => 'application/x-apple-diskimage',
'pkg' => 'application/x-newton-compatible-pkg',
// Прочие
'torrent' => 'application/x-bittorrent',
'ics' => 'text/calendar',
'vcf' => 'text/vcard',
'gpx' => 'application/gpx+xml',
'kml' => 'application/vnd.google-earth.kml+xml',
'kmz' => 'application/vnd.google-earth.kmz'
];
$mimeType = $mimeTypes[$extension] ?? 'application/octet-stream';
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . filesize($filepath));
header('Content-Disposition: inline; filename="' . $filename . '"');
header('Cache-Control: public, max-age=31536000'); // Кеш на год
readfile($filepath);
exit;
} else {
http_response_code(404);
echo '404 - Файл не найден';
exit;
}
echo '<form method="post">
<h3>Login</h3>
<input type="password" name="password" placeholder="Password">
<button type="submit">Login</button>
</form>';
exit;
}
// Удаление
if (isset($_GET['delete'])) {
$f = basename($_GET['delete']);
@unlink(UPLOAD_DIR . $f);
header("Location: index.php");
exit;
}
// Проверка авторизации
if (!isset($_SESSION['logged_in']) || ($_GET['action'] ?? '') === 'logout') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$password = $_POST['password'] ?? '';
$csrf_token = $_POST['csrf_token'] ?? '';
$files = array_diff(scandir(UPLOAD_DIR), ['.', '..']);
?><!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Файловое хранилище</title></head>
<body>
<h3>Файловое хранилище</h3>
<p><a href="?action=logout">Выйти</a></p>
<input type="file" id="fileInput">
<div id="progress"></div>
<ul>
<?php foreach ($files as $file): $url = 'upload/' . rawurlencode($file); ?>
<li>
<?= htmlspecialchars($file) ?>
[<a href="<?= $url ?>" target="_blank">Скачать</a>]
[<a href="?delete=<?= urlencode($file) ?>" onclick="return confirm('Удалить?')">Удалить</a>]
<button onclick="copyLink('<?= $url ?>')">Копировать ссылку</button>
</li>
<?php endforeach; ?>
</ul>
<script>
function copyLink(link) {
navigator.clipboard.writeText(location.origin + '/' + link).then(() => alert("Скопировано"));
}
const input = document.getElementById('fileInput');
input.addEventListener('change', async () => {
const file = input.files[0];
if (!file) return;
const chunkSize = 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < totalChunks; i++) {
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);
await fetch('upload_chunk.php', { method: 'POST', body: formData });
document.getElementById('progress').innerText = `Загружено: ${i+1}/${totalChunks}`;
// Проверяем CSRF токен
if (!verifyCSRFToken($csrf_token)) {
$error = 'Ошибка безопасности. Попробуйте еще раз.';
} elseif (password_verify($password, PASSWORD_HASH)) {
$_SESSION['logged_in'] = true;
header("Location: index.php");
exit;
} else {
$error = 'Неверный пароль';
}
}
await fetch('merge_chunks.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, total: totalChunks })
});
$csrf_token = generateCSRFToken();
alert('Готово!');
location.reload();
});
</script>
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 = 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 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;
}
// Проверка расширения
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>