Files
simple-file-share/index.php

573 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
// Обработка запросов файлов напрямую из корня
$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'])
) {
$requestedFile = rawurldecode($requestedFile);
$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;
}
}
// Проверка авторизации
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 = 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>