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

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
upload/
upload_chunks/

View File

@@ -1,4 +1,72 @@
<?php <?php
define('PASSWORD', 'your_password_here'); // Хешированный пароль (используйте password_hash() для генерации)
// Пример для пароля "mypassword123": password_hash('mypassword123', PASSWORD_DEFAULT)
define('PASSWORD_HASH', '$2y$12$54SMO7iJ/W2d3LKFGviGEuzD67qlsLpfGxYAh7rS5TEcG5hVO3bP.'); // замените на свой хеш
define('UPLOAD_DIR', __DIR__ . '/upload/'); define('UPLOAD_DIR', __DIR__ . '/upload/');
define('CHUNK_DIR', __DIR__ . '/upload_chunks/'); define('CHUNK_DIR', __DIR__ . '/upload_chunks/');
// Максимальный размер файла (можно указать как '10GB', '500MB', '50KB')
define('MAX_FILE_SIZE', '10GB');
// Размер чанка для загрузки (можно указать как '10MB', '1MB', '512KB')
define('CHUNK_SIZE', '2MB');
// Разрешенные расширения файлов (whitelist)
define('ALLOWED_EXTENSIONS', [
// Изображения
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'tiff', 'tif', 'ico', 'heic', 'heif', 'avif',
// Документы
'pdf', 'doc', 'docx', 'txt', 'rtf', 'odt', 'ods', 'odp', 'pages', 'key', 'numbers',
'ppt', 'pptx', 'xps', 'epub', 'mobi', 'fb2',
// Видео
'mp4', 'avi', 'mkv', 'mov', 'webm', 'flv', 'wmv', 'mpg', 'mpeg', 'm4v', '3gp', 'ogv', 'ts', 'mts',
// Аудио
'mp3', 'wav', 'flac', 'ogg', 'aac', 'm4a', 'wma', 'opus', 'oga', 'aiff', 'au', 'ra',
// Архивы
'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'lzma', 'cab', 'iso', 'dmg', 'deb', 'rpm',
// Данные и таблицы
'json', 'xml', 'csv', 'xls', 'xlsx', 'ods', 'tsv', 'yaml', 'yml', 'toml', 'ini', 'conf', 'cfg',
// Код и разработка
'html', 'htm', 'css', 'js', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'swift',
'kt', 'scala', 'pl', 'sh', 'bat', 'cmd', 'ps1', 'vbs', 'r', 'sql', 'md', 'tex', 'log',
// Веб и CMS
'wpress', 'sql', 'backup', 'bak', 'db', 'sqlite', 'sqlite3',
// Шрифты
'ttf', 'otf', 'woff', 'woff2', 'eot',
// CAD и дизайн
'dwg', 'dxf', 'skp', 'blend', 'obj', 'fbx', '3ds', 'dae', 'stl', 'ply',
'psd', 'ai', 'eps', 'indd', 'sketch', 'fig', 'xd',
// Виртуализация и образы
'vmdk', 'vdi', 'vhd', 'vhdx', 'qcow2', 'img', 'bin', 'cue',
// Мобильные приложения и исполняемые файлы
'apk', 'ipa', 'appx', 'msix', 'exe', 'msi', 'deb', 'rpm', 'dmg', 'pkg',
// Научные данные
'mat', 'hdf5', 'nc', 'fits', 'sdf', 'mol', 'pdb',
// Игры и мультимедиа
'unity', 'unitypackage', 'pak', 'wad', 'pk3', 'vpk',
// Электронные книги и документы
'djvu', 'chm', 'hlp',
// Прочие полезные форматы
'torrent', 'magnet', 'desktop', 'lnk', 'url',
'ics', 'vcf', 'vcard', 'gpx', 'kml', 'kmz'
]);
// В конце файла config.php добавьте:
error_log("Config loaded. MAX_FILE_SIZE: " . MAX_FILE_SIZE);
error_log("Config loaded. CHUNK_SIZE: " . CHUNK_SIZE);

146
functions.php Normal file
View File

@@ -0,0 +1,146 @@
<?php
// Функция для преобразования размера из строки в байты
function parseFileSize($size)
{
if (is_numeric($size)) {
return intval($size);
}
$size = strtoupper(trim($size));
// Проверяем, что строка не пустая
if (empty($size)) {
return 0;
}
// Добавляем отладочную информацию
error_log("parseFileSize input: " . $size);
$units = [
'TB' => 1024 ** 4,
'GB' => 1024 ** 3,
'MB' => 1024 ** 2,
'KB' => 1024,
'B' => 1
];
// Проверяем каждую единицу измерения (начиная с самых больших)
foreach ($units as $unit => $multiplier) {
if (substr($size, -strlen($unit)) === $unit) {
$numberPart = trim(substr($size, 0, -strlen($unit)));
error_log("Found unit: $unit, number part: '$numberPart'");
if (is_numeric($numberPart)) {
$result = intval(floatval($numberPart) * $multiplier);
error_log("Calculated result: $result bytes");
return $result;
}
}
}
// Если не найдено совпадений с единицами измерения, пробуем преобразовать как число
if (is_numeric($size)) {
$result = intval($size);
error_log("Fallback to numeric: $result");
return $result;
}
error_log("Could not parse size: $size, returning 0");
return 0;
}
// Получить размер файла в байтах
function getMaxFileSize()
{
$size = parseFileSize(MAX_FILE_SIZE);
// Отладочная информация
error_log("MAX_FILE_SIZE constant: " . MAX_FILE_SIZE);
error_log("Parsed max file size: " . $size);
return $size;
}
// Получить размер чанка в байтах
function getChunkSize()
{
$size = parseFileSize(CHUNK_SIZE);
// Отладочная информация
error_log("CHUNK_SIZE constant: " . CHUNK_SIZE);
error_log("Parsed chunk size: " . $size);
return $size;
}
// Функция для генерации CSRF токена
function generateCSRFToken()
{
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Функция для проверки CSRF токена
function verifyCSRFToken($token)
{
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
// Функция для валидации имени файла
function validateFileName($filename)
{
// Удаляем опасные символы и пути
$filename = basename($filename);
// Проверяем на недопустимые символы
if (preg_match('/[<>:"|?*\x00-\x1f]/', $filename)) {
return false;
}
// Проверяем длину
if (strlen($filename) > 255) {
return false;
}
// Проверяем расширение
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($extension, ALLOWED_EXTENSIONS)) {
return false;
}
return true;
}
// Функция для получения безопасного имени файла
function getSafeFileName($filename)
{
$filename = basename($filename);
$filename = preg_replace('/[<>:"|?*\x00-\x1f]/', '_', $filename);
return substr($filename, 0, 255);
}
// Функция для форматирования размера файла
function formatFileSize($size)
{
$units = ['B', 'KB', 'MB', 'GB'];
$unitIndex = 0;
while ($size >= 1024 && $unitIndex < count($units) - 1) {
$size /= 1024;
$unitIndex++;
}
return round($size, 2) . ' ' . $units[$unitIndex];
}
// Функция для отладки - показать все константы
function debugConstants()
{
error_log("=== DEBUG CONSTANTS ===");
error_log("MAX_FILE_SIZE: " . (defined('MAX_FILE_SIZE') ? MAX_FILE_SIZE : 'NOT DEFINED'));
error_log("CHUNK_SIZE: " . (defined('CHUNK_SIZE') ? CHUNK_SIZE : 'NOT DEFINED'));
error_log("UPLOAD_DIR: " . (defined('UPLOAD_DIR') ? UPLOAD_DIR : 'NOT DEFINED'));
error_log("CHUNK_DIR: " . (defined('CHUNK_DIR') ? CHUNK_DIR : 'NOT DEFINED'));
error_log("======================");
}

627
index.php
View File

@@ -2,86 +2,571 @@
session_start(); session_start();
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/config.php';
require_once __DIR__ . '/functions.php';
if (!file_exists(UPLOAD_DIR)) mkdir(UPLOAD_DIR); if (!file_exists(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
if (!file_exists(CHUNK_DIR)) mkdir(CHUNK_DIR); 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) { $requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$_SESSION['logged_in'] = true; $requestedFile = ltrim($requestUri, '/');
header("Location: index.php");
// Если запрашивается файл (не административный интерфейс)
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; 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'])) { if (!isset($_SESSION['logged_in']) || ($_GET['action'] ?? '') === 'logout') {
$f = basename($_GET['delete']); if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@unlink(UPLOAD_DIR . $f); $password = $_POST['password'] ?? '';
header("Location: index.php"); $csrf_token = $_POST['csrf_token'] ?? '';
exit;
}
$files = array_diff(scandir(UPLOAD_DIR), ['.', '..']); // Проверяем CSRF токен
?><!DOCTYPE html> if (!verifyCSRFToken($csrf_token)) {
<html> $error = 'Ошибка безопасности. Попробуйте еще раз.';
<head><meta charset="utf-8"><title>Файловое хранилище</title></head> } elseif (password_verify($password, PASSWORD_HASH)) {
<body> $_SESSION['logged_in'] = true;
<h3>Файловое хранилище</h3> header("Location: index.php");
<p><a href="?action=logout">Выйти</a></p> exit;
<input type="file" id="fileInput"> } else {
<div id="progress"></div> $error = 'Неверный пароль';
<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}`;
} }
await fetch('merge_chunks.php', { $csrf_token = generateCSRFToken();
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, total: totalChunks })
});
alert('Готово!'); echo '<!DOCTYPE html>
location.reload(); <html>
}); <head>
</script> <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> </body>
</html> </html>

View File

@@ -1,21 +1,188 @@
<?php <?php
require_once __DIR__ . '/config.php'; // Включаем отображение всех ошибок для отладки
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
$data = json_decode(file_get_contents('php://input'), true); session_start();
$filename = basename($data['filename']);
$total = intval($data['total']);
$tmpDir = __DIR__ . '/upload_chunks/' . $filename;
$finalPath = __DIR__ . '/upload/' . $filename;
$out = fopen($finalPath, 'w'); try {
for ($i = 0; $i < $total; $i++) { error_log("=== MERGE CHUNKS START ===");
$partPath = "$tmpDir/$i.part";
$in = fopen($partPath, 'r'); require_once __DIR__ . '/config.php';
stream_copy_to_stream($in, $out); require_once __DIR__ . '/functions.php';
fclose($in);
unlink($partPath); // Проверка авторизации
if (!isset($_SESSION['logged_in'])) {
error_log("ERROR: User not logged in");
http_response_code(403);
echo "Not authorized";
exit;
}
// Получение и проверка JSON данных
$input = file_get_contents('php://input');
error_log("Input data: " . $input);
$data = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("ERROR: JSON decode error: " . json_last_error_msg());
http_response_code(400);
echo "Invalid JSON data";
exit;
}
error_log("Decoded data: " . print_r($data, true));
// Проверка CSRF токена
$csrf_token = $data['csrf_token'] ?? '';
if (!verifyCSRFToken($csrf_token)) {
error_log("ERROR: CSRF token verification failed");
http_response_code(403);
echo "CSRF token verification failed";
exit;
}
$filename = getSafeFileName($data['filename'] ?? '');
$total = intval($data['total'] ?? 0);
error_log("Processing merge: filename='$filename', total=$total");
// Валидация данных
if (empty($filename) || $total <= 0) {
error_log("ERROR: Invalid parameters");
http_response_code(400);
echo "Invalid parameters";
exit;
}
// Валидация имени файла
if (!validateFileName($filename)) {
error_log("ERROR: Invalid filename: $filename");
http_response_code(400);
echo "Invalid filename";
exit;
}
$tmpDir = CHUNK_DIR . $filename;
$finalPath = UPLOAD_DIR . $filename;
error_log("Chunk directory: $tmpDir");
error_log("Final path: $finalPath");
// Проверка существования директории с чанками
if (!file_exists($tmpDir)) {
error_log("ERROR: Chunk directory does not exist: $tmpDir");
http_response_code(400);
echo "Chunk directory not found";
exit;
}
// Проверка всех чанков и подсчет общего размера
$totalSize = 0;
$missingChunks = [];
for ($i = 0; $i < $total; $i++) {
$partPath = "$tmpDir/$i.part";
if (!file_exists($partPath)) {
$missingChunks[] = $i;
} else {
$chunkSize = filesize($partPath);
$totalSize += $chunkSize;
error_log("Chunk $i size: $chunkSize bytes");
}
}
if (!empty($missingChunks)) {
error_log("ERROR: Missing chunks: " . implode(', ', $missingChunks));
http_response_code(400);
echo "Missing chunks: " . implode(', ', $missingChunks);
exit;
}
error_log("Total file size: $totalSize bytes");
error_log("Max allowed size: " . getMaxFileSize() . " bytes");
// Проверка размера файла
if ($totalSize > getMaxFileSize()) {
error_log("ERROR: File too large");
// Очистка чанков
for ($i = 0; $i < $total; $i++) {
@unlink("$tmpDir/$i.part");
}
@rmdir($tmpDir);
http_response_code(413); // Payload Too Large
echo "File too large";
exit;
}
// Проверка прав на запись в директорию загрузок
if (!is_writable(UPLOAD_DIR)) {
error_log("ERROR: Upload directory is not writable: " . UPLOAD_DIR);
http_response_code(500);
echo "Upload directory is not writable";
exit;
}
// Объединение чанков
error_log("Starting file merge");
$out = fopen($finalPath, 'wb');
if (!$out) {
error_log("ERROR: Cannot create final file: $finalPath");
http_response_code(500);
echo "Cannot create final file";
exit;
}
for ($i = 0; $i < $total; $i++) {
$partPath = "$tmpDir/$i.part";
error_log("Merging chunk $i from $partPath");
$in = fopen($partPath, 'rb');
if (!$in) {
error_log("ERROR: Cannot open chunk $i");
fclose($out);
@unlink($finalPath);
http_response_code(500);
echo "Cannot open chunk $i";
exit;
}
$copied = stream_copy_to_stream($in, $out);
fclose($in);
error_log("Copied $copied bytes from chunk $i");
// Удаление чанка после успешного копирования
if (!unlink($partPath)) {
error_log("WARNING: Cannot delete chunk $i");
}
}
fclose($out);
// Удаление директории чанков
if (!rmdir($tmpDir)) {
error_log("WARNING: Cannot delete chunk directory: $tmpDir");
}
$finalSize = filesize($finalPath);
error_log("Final file size: $finalSize bytes");
error_log("SUCCESS: File merged successfully");
http_response_code(200);
echo "OK";
error_log("=== MERGE CHUNKS END ===");
} catch (Exception $e) {
error_log("EXCEPTION in merge_chunks.php: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
http_response_code(500);
echo "Server error: " . $e->getMessage();
} catch (Error $e) {
error_log("FATAL ERROR in merge_chunks.php: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
http_response_code(500);
echo "Fatal error: " . $e->getMessage();
} }
fclose($out);
rmdir($tmpDir);
http_response_code(200);

432
style.css Normal file
View File

@@ -0,0 +1,432 @@
/* Сброс и базовые стили */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Цветовая палитра */
--primary: #2563eb;
--primary-hover: #1d4ed8;
--success: #059669;
--success-light: #d1fae5;
--error: #dc2626;
--error-light: #fee2e2;
--warning: #d97706;
--warning-light: #fef3c7;
/* Нейтральные цвета */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
/* Размеры и отступы */
--border-radius: 8px;
--border-radius-lg: 12px;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
/* Шрифты */
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', Consolas, 'Courier New', monospace;
}
body {
font-family: var(--font-system);
font-size: 15px;
line-height: 1.6;
color: var(--gray-700);
background: var(--gray-50);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1rem;
}
/* Контейнер */
.container {
width: 100%;
max-width: 720px;
background: white;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
overflow: hidden;
}
/* Заголовок */
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
color: white;
padding: 2rem;
text-align: center;
}
.header h1 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.header .logout {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.2s ease;
}
.header .logout:hover {
color: white;
}
/* Основной контент */
.content {
padding: 2rem;
}
/* Форма входа */
.login-form {
max-width: 400px;
margin: 4rem auto;
background: white;
padding: 2.5rem;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
}
.login-form h3 {
font-size: 1.5rem;
font-weight: 600;
text-align: center;
margin-bottom: 2rem;
color: var(--gray-800);
}
/* Уведомления */
.notification {
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
border-radius: var(--border-radius);
border: 1px solid;
font-size: 0.9rem;
font-weight: 500;
display: none;
animation: slideIn 0.3s ease-out;
}
.notification.success {
background-color: var(--success-light);
border-color: var(--success);
color: #065f46;
}
.notification.error {
background-color: var(--error-light);
border-color: var(--error);
color: #991b1b;
}
.notification.info {
background-color: #dbeafe;
border-color: var(--primary);
color: #1e40af;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Поля ввода */
input[type="password"],
input[type="file"] {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--gray-200);
border-radius: var(--border-radius);
font-size: 0.95rem;
transition: all 0.2s ease;
background: white;
}
input[type="password"]:focus,
input[type="file"]:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
input[type="password"] {
margin-bottom: 1.5rem;
}
/* Кнопки */
button,
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
font-weight: 500;
border-radius: var(--border-radius);
border: none;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
gap: 0.5rem;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow);
}
.btn-secondary {
background: var(--gray-100);
color: var(--gray-700);
border: 1px solid var(--gray-200);
}
.btn-secondary:hover {
background: var(--gray-200);
border-color: var(--gray-300);
}
.btn-small {
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
}
.btn-danger {
background: var(--error);
color: white;
}
.btn-danger:hover {
background: #b91c1c;
}
/* Область загрузки */
.upload-area {
margin-bottom: 2rem;
}
.upload-zone {
border: 2px dashed var(--gray-300);
border-radius: var(--border-radius);
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
overflow: hidden;
}
.upload-zone:hover {
border-color: var(--primary);
background: rgba(37, 99, 235, 0.02);
}
.upload-zone.dragover {
border-color: var(--primary);
background: rgba(37, 99, 235, 0.05);
transform: scale(1.02);
}
.file-input-hidden {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.upload-text {
color: var(--gray-500);
font-size: 0.9rem;
}
.upload-text strong {
color: var(--primary);
}
/* Прогресс загрузки */
.progress-container {
margin-top: 1rem;
display: none;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--gray-200);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--primary-hover));
border-radius: 4px;
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
font-size: 0.8rem;
color: var(--gray-600);
text-align: center;
margin-top: 0.5rem;
}
/* Список файлов */
.files-list {
list-style: none;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
margin-bottom: 0.75rem;
background: white;
transition: all 0.2s ease;
}
.file-item:hover {
border-color: var(--gray-300);
box-shadow: var(--shadow-sm);
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 500;
color: var(--gray-800);
margin-bottom: 0.25rem;
word-break: break-all;
}
.file-size {
font-size: 0.8rem;
color: var(--gray-500);
font-family: var(--font-mono);
}
.file-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
margin-left: 1rem;
}
.file-actions .btn {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
/* Пустое состояние */
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: var(--gray-500);
}
.empty-state svg {
width: 48px;
height: 48px;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Адаптивность */
@media (max-width: 640px) {
body {
padding: 1rem 0.5rem;
}
.container {
border-radius: 0;
box-shadow: none;
min-height: 100vh;
}
.header,
.content {
padding: 1.5rem;
}
.file-item {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.file-actions {
margin-left: 0;
align-self: stretch;
}
.file-actions .btn {
flex: 1;
}
.upload-zone {
padding: 1.5rem 1rem;
}
}
/* Темные акценты для интерактивных элементов */
input[type="file"]::-webkit-file-upload-button {
background: var(--primary);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: calc(var(--border-radius) - 2px);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
margin-right: 1rem;
}
input[type="file"]::-webkit-file-upload-button:hover {
background: var(--primary-hover);
}
/* Анимации */
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

View File

@@ -1,8 +1,118 @@
<?php <?php
$filename = basename($_POST['filename']); // Включаем отображение всех ошибок для отладки
$index = intval($_POST['index']); error_reporting(E_ALL);
$tmpDir = __DIR__ . '/upload_chunks/' . $filename; ini_set('display_errors', 1);
if (!file_exists($tmpDir)) mkdir($tmpDir, 0777, true); ini_set('log_errors', 1);
move_uploaded_file($_FILES['chunk']['tmp_name'], "$tmpDir/$index.part"); session_start();
try {
error_log("=== UPLOAD CHUNK START ===");
error_log("POST data: " . print_r($_POST, true));
error_log("FILES data: " . print_r($_FILES, true));
error_log("Session data: " . print_r($_SESSION, true));
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/functions.php';
// Проверка авторизации
if (!isset($_SESSION['logged_in'])) {
error_log("ERROR: User not logged in");
http_response_code(403);
echo "Not authorized";
exit;
}
// Проверка CSRF токена
$csrf_token = $_POST['csrf_token'] ?? '';
error_log("CSRF token from POST: " . $csrf_token);
error_log("CSRF token from session: " . ($_SESSION['csrf_token'] ?? 'NOT SET'));
if (!verifyCSRFToken($csrf_token)) {
error_log("ERROR: CSRF token verification failed");
http_response_code(403);
echo "CSRF token verification failed";
exit;
}
// Проверка данных POST
if (!isset($_POST['filename']) || !isset($_POST['index'])) {
error_log("ERROR: Missing POST parameters");
http_response_code(400);
echo "Missing parameters";
exit;
}
// Проверка загруженного файла
if (!isset($_FILES['chunk']) || $_FILES['chunk']['error'] !== UPLOAD_ERR_OK) {
error_log("ERROR: File upload error. Error code: " . ($_FILES['chunk']['error'] ?? 'NO_FILE'));
http_response_code(400);
echo "File upload error: " . ($_FILES['chunk']['error'] ?? 'NO_FILE');
exit;
}
$filename = getSafeFileName($_POST['filename']);
$index = intval($_POST['index']);
error_log("Processing chunk: filename='$filename', index=$index");
// Валидация имени файла
if (!validateFileName($filename)) {
error_log("ERROR: Invalid filename: $filename");
http_response_code(400);
echo "Invalid filename";
exit;
}
// Создание директории для чанков
$tmpDir = CHUNK_DIR . $filename;
error_log("Chunk directory: $tmpDir");
if (!file_exists($tmpDir)) {
if (!mkdir($tmpDir, 0755, true)) {
error_log("ERROR: Cannot create chunk directory: $tmpDir");
http_response_code(500);
echo "Cannot create chunk directory";
exit;
}
error_log("Created chunk directory: $tmpDir");
}
// Проверка прав на запись
if (!is_writable($tmpDir)) {
error_log("ERROR: Chunk directory is not writable: $tmpDir");
http_response_code(500);
echo "Chunk directory is not writable";
exit;
}
$chunkPath = "$tmpDir/$index.part";
error_log("Chunk path: $chunkPath");
error_log("Temporary file: " . $_FILES['chunk']['tmp_name']);
error_log("Chunk size: " . $_FILES['chunk']['size']);
// Перемещение загруженного чанка
if (move_uploaded_file($_FILES['chunk']['tmp_name'], $chunkPath)) {
error_log("SUCCESS: Chunk saved to $chunkPath");
http_response_code(200);
echo "OK";
} else {
error_log("ERROR: Cannot move uploaded file to $chunkPath");
error_log("PHP error: " . error_get_last()['message'] ?? 'Unknown error');
http_response_code(500);
echo "Cannot save chunk";
}
error_log("=== UPLOAD CHUNK END ===");
} catch (Exception $e) {
error_log("EXCEPTION in upload_chunk.php: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
http_response_code(500);
echo "Server error: " . $e->getMessage();
} catch (Error $e) {
error_log("FATAL ERROR in upload_chunk.php: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
http_response_code(500);
echo "Fatal error: " . $e->getMessage();
}