This commit is contained in:
2026-05-24 04:17:53 +03:00
parent fccf7b1ea3
commit cb5f0c498e
5 changed files with 905 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
<?php
session_start();
if (file_exists('aws.phar')) {
require 'aws.phar';
} else {
die("Требуется aws.phar (версия 3)");
}
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
$db_file = __DIR__ . '/datas.db';
if (!file_exists($db_file)) die("База данных не найдена. Сначала запустите установку.");
$db = new PDO('sqlite:' . $db_file);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Выход
if (isset($_GET['logout'])) {
session_destroy();
header("Location: /ui");
exit;
}
// Авторизация
if (isset($_POST['login'])) {
$stmt = $db->prepare("SELECT * FROM s3_mounts WHERE dav_user = ?");
$stmt->execute([$_POST['username']]);
$user = $stmt->fetch();
if ($user && password_verify($_POST['password'], $user['dav_pass'])) {
$_SESSION['ui_user'] = $user;
header("Location: /ui");
exit;
} else {
sleep(2); // Защита от брутфорса
$error = "Неверный логин или пароль";
}
}
// Экран входа
if (!isset($_SESSION['ui_user'])) {
?>
<!DOCTYPE html>
<html lang="ru" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход | S3 Web UI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
</head>
<body class="d-flex align-items-center py-4 bg-body-tertiary" style="height: 100vh;">
<main class="form-signin w-100 m-auto" style="max-width: 400px;">
<div class="card shadow-sm">
<div class="card-body p-4 text-center">
<i class="bi bi-cloud-arrow-up text-primary" style="font-size: 3rem;"></i>
<h1 class="h3 mb-3 fw-normal mt-2">Вход в хранилище</h1>
<?php if(isset($error)) echo "<div class='alert alert-danger'>$error</div>"; ?>
<form method="POST">
<div class="form-floating mb-2">
<input type="text" class="form-control" id="floatingInput" name="username" placeholder="Логин" required>
<label for="floatingInput">Логин (WebDAV)</label>
</div>
<div class="form-floating mb-3">
<input type="password" class="form-control" id="floatingPassword" name="password" placeholder="Пароль" required>
<label for="floatingPassword">Пароль</label>
</div>
<button class="btn btn-primary w-100 py-2" type="submit" name="login"><i class="bi bi-box-arrow-in-right"></i> Войти</button>
</form>
</div>
</div>
</main>
</body>
</html>
<?php
exit;
}
$mount = $_SESSION['ui_user'];
// Инициализация S3
$s3 = new S3Client([
'version' => 'latest',
'region' => $mount['s3_region'],
'endpoint' => $mount['s3_endpoint'],
'credentials' => [
'key' => $mount['s3_key'],
'secret' => $mount['s3_secret'],
],
'use_path_style_endpoint' => true
]);
$bucket = $mount['s3_bucket'];
$prefix = isset($_GET['path']) ? $_GET['path'] : '';
if ($prefix !== '' && substr($prefix, -1) !== '/') $prefix .= '/';
// --- Обработка действий ---
try {
if (isset($_FILES['file'])) {
$s3->putObject([
'Bucket' => $bucket,
'Key' => $prefix . $_FILES['file']['name'],
'SourceFile' => $_FILES['file']['tmp_name']
]);
header("Location: /ui?path=" . urlencode($prefix));
exit;
}
if (isset($_POST['new_folder'])) {
$folderName = trim($_POST['new_folder']);
if (!empty($folderName)) {
$s3->putObject(['Bucket' => $bucket, 'Key' => $prefix . $folderName . '/', 'Body' => '']);
}
header("Location: /ui?path=" . urlencode($prefix));
exit;
}
if (isset($_POST['delete_key'])) {
$s3->deleteObject(['Bucket' => $bucket, 'Key' => $_POST['delete_key']]);
header("Location: /ui?path=" . urlencode($prefix));
exit;
}
} catch (AwsException $e) {
$action_error = "Ошибка S3: " . $e->getMessage();
}
// Получение списка файлов
$objects = [];
$folders = [];
try {
$result = $s3->listObjectsV2([
'Bucket' => $bucket,
'Prefix' => $prefix,
'Delimiter' => '/'
]);
if (isset($result['CommonPrefixes'])) {
foreach ($result['CommonPrefixes'] as $p) $folders[] = $p['Prefix'];
}
if (isset($result['Contents'])) {
foreach ($result['Contents'] as $c) {
if ($c['Key'] !== $prefix) $objects[] = $c;
}
}
} catch (AwsException $e) {
die("Ошибка подключения к S3: " . $e->getMessage());
}
function getPresignedUrl($s3, $bucket, $key) {
$cmd = $s3->getCommand('GetObject', ['Bucket' => $bucket, 'Key' => $key]);
$request = $s3->createPresignedRequest($cmd, '+2 hours');
return (string) $request->getUri();
}
// Хлебные крошки
$pathParts = array_filter(explode('/', $prefix));
$breadcrumbsHTML = "<li class='breadcrumb-item'><a href='/ui'><i class='bi bi-house-door'></i> Корень</a></li>";
$accPath = '';
foreach ($pathParts as $part) {
$accPath .= $part . '/';
$breadcrumbsHTML .= "<li class='breadcrumb-item'><a href='/ui?path=" . urlencode($accPath) . "'>" . htmlspecialchars($part) . "</a></li>";
}
// Категории файлов для UI
$imgExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
$vidExt = ['mp4', 'webm', 'ogg', 'mov', 'avi'];
$audExt = ['mp3', 'wav', 'ogg', 'flac'];
$txtExt = ['txt', 'md', 'json', 'csv', 'log', 'xml', 'php', 'html', 'css', 'js'];
?>
<!DOCTYPE html>
<html lang="ru" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>S3 Файловый менеджер</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
.icon-large { font-size: 1.5rem; vertical-align: middle; }
.table-hover tbody tr:hover { cursor: pointer; }
/* Исправление белого фона для txt во фрейме */
.text-iframe { width: 100%; height: 60vh; border: none; background: #fff; border-radius: 8px;}
</style>
</head>
<body class="bg-body-tertiary">
<!-- Навбар -->
<nav class="navbar navbar-expand-lg bg-dark border-bottom border-secondary mb-4 shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="#"><i class="bi bi-cloud-check text-primary"></i> <?= htmlspecialchars($mount['s3_bucket']) ?></a>
<div class="d-flex align-items-center">
<span class="text-light me-3"><i class="bi bi-person-circle"></i> <?= htmlspecialchars($mount['dav_user']) ?></span>
<a href="?logout=1" class="btn btn-sm btn-outline-danger"><i class="bi bi-box-arrow-right"></i> Выйти</a>
</div>
</div>
</nav>
<div class="container-fluid px-4">
<?php if(isset($action_error)) echo "<div class='alert alert-danger'>$action_error</div>"; ?>
<!-- Панель управления (Загрузка / Папки) -->
<div class="row mb-3 g-2">
<div class="col-12 col-md-6">
<form method="POST" enctype="multipart/form-data" class="d-flex">
<input type="file" name="file" class="form-control me-2" required>
<button type="submit" class="btn btn-primary text-nowrap"><i class="bi bi-upload"></i> Загрузить</button>
</form>
</div>
<div class="col-12 col-md-6">
<form method="POST" class="input-group">
<input type="text" name="new_folder" class="form-control" placeholder="Имя новой папки" required>
<button type="submit" class="btn btn-success"><i class="bi bi-folder-plus"></i> Создать</button>
</form>
</div>
</div>
<!-- Основная карточка с файлами -->
<div class="card shadow-sm mb-5">
<div class="card-header bg-dark">
<nav aria-label="breadcrumb" class="mt-2">
<ol class="breadcrumb mb-0">
<?= $breadcrumbsHTML ?>
</ol>
</nav>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-dark">
<tr>
<th scope="col" style="width: 50%;">Имя</th>
<th scope="col">Размер</th>
<th scope="col" class="d-none d-md-table-cell">Изменен</th>
<th scope="col" class="text-end">Действия</th>
</tr>
</thead>
<tbody>
<!-- Папки -->
<?php foreach ($folders as $f):
$folderName = basename($f);
?>
<tr>
<td>
<a href="/ui?path=<?= urlencode($f) ?>" class="text-decoration-none text-light fw-bold">
<i class="bi bi-folder-fill text-warning icon-large me-2"></i> <?= htmlspecialchars($folderName) ?>
</a>
</td>
<td class="text-muted">-</td>
<td class="text-muted d-none d-md-table-cell">-</td>
<td class="text-end">
<form method="POST" class="d-inline" onsubmit="return confirm('Удалить папку?');">
<input type="hidden" name="delete_key" value="<?= htmlspecialchars($f) ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<!-- Файлы -->
<?php foreach ($objects as $obj):
$fileName = basename($obj['Key']);
$size = round($obj['Size'] / 1024 / 1024, 2) . ' MB';
$date = date('d.m.Y H:i', strtotime($obj['LastModified']->__toString()));
$url = getPresignedUrl($s3, $bucket, $obj['Key']);
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
// Определяем тип файла для иконок и логики открытия
$viewType = 'download';
$icon = 'bi-file-earmark text-secondary';
if (in_array($ext, $imgExt)) { $viewType = 'image'; $icon = 'bi-file-image text-info'; }
elseif (in_array($ext, $vidExt)) { $viewType = 'video'; $icon = 'bi-file-play-fill text-primary'; }
elseif (in_array($ext, $audExt)) { $viewType = 'audio'; $icon = 'bi-file-music text-success'; }
elseif (in_array($ext, $txtExt)) { $viewType = 'text'; $icon = 'bi-file-text text-light'; }
?>
<tr>
<td>
<i class="bi <?= $icon ?> icon-large me-2"></i>
<?php if($viewType !== 'download'): ?>
<a href="javascript:void(0)" onclick="openPreview('<?= htmlspecialchars($url) ?>', '<?= $viewType ?>', '<?= htmlspecialchars($fileName) ?>', '<?= $ext ?>')" class="text-decoration-none text-light">
<?= htmlspecialchars($fileName) ?>
</a>
<?php else: ?>
<span class="text-light"><?= htmlspecialchars($fileName) ?></span>
<?php endif; ?>
</td>
<td><span class="badge bg-secondary"><?= $size ?></span></td>
<td class="text-muted d-none d-md-table-cell"><small><?= $date ?></small></td>
<td class="text-end text-nowrap">
<?php if($viewType !== 'download'): ?>
<button class="btn btn-sm btn-outline-primary" onclick="openPreview('<?= htmlspecialchars($url) ?>', '<?= $viewType ?>', '<?= htmlspecialchars($fileName) ?>', '<?= $ext ?>')"><i class="bi bi-eye"></i></button>
<?php endif; ?>
<a href="<?= htmlspecialchars($url) ?>" target="_blank" class="btn btn-sm btn-outline-success"><i class="bi bi-download"></i></a>
<form method="POST" class="d-inline" onsubmit="return confirm('Удалить файл?');">
<input type="hidden" name="delete_key" value="<?= htmlspecialchars($obj['Key']) ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($folders) && empty($objects)): ?>
<tr><td colspan="4" class="text-center py-5 text-muted"><i class="bi bi-inbox fs-1 d-block mb-2"></i> Папка пуста</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Модальное окно предпросмотра -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-truncate" id="previewTitle" style="max-width: 90%;">Файл</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body text-center p-0 bg-black rounded-bottom" id="previewBody" style="overflow: hidden;">
<!-- Контент загружается через JS -->
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
const previewModalEl = document.getElementById('previewModal');
const previewBody = document.getElementById('previewBody');
const previewTitle = document.getElementById('previewTitle');
// Остановка медиа при закрытии окна
previewModalEl.addEventListener('hidden.bs.modal', () => {
previewBody.innerHTML = '';
});
function openPreview(url, type, filename, ext) {
previewTitle.innerText = filename;
let html = '';
if (type === 'image') {
html = `<img src="${url}" class="img-fluid" style="max-height: 80vh;" alt="preview">`;
} else if (type === 'video') {
html = `<video controls autoplay class="w-100" style="max-height: 80vh;"><source src="${url}" type="video/${ext}">Ваш браузер не поддерживает видео.</video>`;
} else if (type === 'audio') {
html = `<div class="p-5 bg-dark"><i class="bi bi-music-note-beamed text-success mb-3 d-block" style="font-size: 4rem;"></i><audio controls autoplay class="w-100"><source src="${url}" type="audio/${ext}"></audio></div>`;
} else if (type === 'text') {
// iframe с белым фоном для текста (т.к. бразуер рендерит TXT черным шрифтом)
html = `<iframe src="${url}" class="text-iframe"></iframe>`;
}
previewBody.innerHTML = html;
previewModal.show();
}
</script>
</body>
</html>