From cb5f0c498e167f8410d9b3d10dd6eacea38eacba Mon Sep 17 00:00:00 2001 From: ai-gen <2+ai-gen@noreply.localhost> Date: Sun, 24 May 2026 04:17:53 +0300 Subject: [PATCH] 1.0.0 --- .htaccess | 15 +++ index.php | 137 ++++++++++++++++++++ install.php | 74 +++++++++++ ui.php | 361 ++++++++++++++++++++++++++++++++++++++++++++++++++++ wd.php | 318 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 905 insertions(+) create mode 100644 .htaccess create mode 100644 index.php create mode 100644 install.php create mode 100644 ui.php create mode 100644 wd.php diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..369fbfc --- /dev/null +++ b/.htaccess @@ -0,0 +1,15 @@ +RewriteEngine On + +# Защита базы данных + + Require all denied + + +# Перенаправление WebDAV +RewriteRule ^wd(/.*)?$ wd.php [L,E=PATH_INFO:$1] + +# Перенаправление UI (Веб-интерфейс пользователя) +RewriteRule ^ui$ ui.php [L] + +# Для поддержки HTTP авторизации на некоторых хостингах +SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..8401cd3 --- /dev/null +++ b/index.php @@ -0,0 +1,137 @@ +install.php"); + +$db = new PDO('sqlite:' . $db_file); +$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +// Авторизация +if (isset($_POST['login'])) { + $stmt = $db->prepare("SELECT password FROM admin WHERE username = ?"); + $stmt->execute([$_POST['username']]); + $row = $stmt->fetch(); + + if ($row && password_verify($_POST['password'], $row['password'])) { + $_SESSION['admin'] = true; + header("Location: index.php"); + exit; + } else { + sleep(2); // Задержка при неверном пароле (мера безопасности) + $error = "Неверный логин или пароль"; + } +} + +// Выход +if (isset($_GET['logout'])) { + session_destroy(); + header("Location: index.php"); + exit; +} + +// Проверка сессии +if (!isset($_SESSION['admin'])) { + ?> + + + + Вход | S3 WebDAV + + + +
+

Вход в панель

+ $error

"; ?> +
+ + + +
+
+ + + prepare("INSERT INTO s3_mounts (dav_user, dav_pass, s3_key, s3_secret, s3_region, s3_endpoint, s3_bucket) VALUES (?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([ + $_POST['dav_user'], password_hash($_POST['dav_pass'], PASSWORD_DEFAULT), + $_POST['s3_key'], $_POST['s3_secret'], $_POST['s3_region'], $_POST['s3_endpoint'], $_POST['s3_bucket'] + ]); + header("Location: index.php"); + exit; +} + +// Удаление S3 +if (isset($_GET['delete'])) { + $stmt = $db->prepare("DELETE FROM s3_mounts WHERE id = ?"); + $stmt->execute([$_GET['delete']]); + header("Location: index.php"); + exit; +} + +$mounts = $db->query("SELECT * FROM s3_mounts")->fetchAll(); +?> + + + + Управление S3 WebDAV + + + +
+

S3 → WebDAV Gateway

+ Выход +
+ +
+

Добавить S3 Подключение (WebDAV User)

+
+ +
+ +
+ +
+
+ +
+
+ +
+

Подключенные S3

+ + + + + + + + + + +
WebDAV ЛогинBucketEndpointДействия
+ Удалить +
+
+ + \ No newline at end of file diff --git a/install.php b/install.php new file mode 100644 index 0000000..93b4424 --- /dev/null +++ b/install.php @@ -0,0 +1,74 @@ + +

Установка системы

+
+
+

+ +
+

+ + +
+ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Таблица администратора UI + $db->exec("CREATE TABLE admin ( + id INTEGER PRIMARY KEY, + username TEXT, + password TEXT + )"); + + // Таблица подключений S3 / WebDAV + $db->exec("CREATE TABLE s3_mounts ( + id INTEGER PRIMARY KEY, + dav_user TEXT UNIQUE, + dav_pass TEXT, + s3_key TEXT, + s3_secret TEXT, + s3_region TEXT, + s3_endpoint TEXT, + s3_bucket TEXT + )"); + + // Хешируем введённый пароль + $hash = password_hash($password, PASSWORD_DEFAULT); + + $stmt = $db->prepare("INSERT INTO admin (username, password) VALUES (:username, :password)"); + $stmt->execute([ + ':username' => $username, + ':password' => $hash + ]); + + echo "

Установка завершена!

"; + echo "

Создан файл datas.db.

"; + echo "

Логин: " . htmlspecialchars($username) . "

"; + echo "

В целях безопасности удалите файл install.php!

"; + echo "Перейти в панель управления"; + +} catch (Exception $e) { + die("Ошибка БД: " . $e->getMessage()); +} \ No newline at end of file diff --git a/ui.php b/ui.php new file mode 100644 index 0000000..b594f04 --- /dev/null +++ b/ui.php @@ -0,0 +1,361 @@ +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'])) { + ?> + + + + + + Вход | S3 Web UI + + + + +
+
+
+ +

Вход в хранилище

+ $error
"; ?> +
+
+ + +
+
+ + +
+ +
+
+ +
+ + + '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 = ""; +$accPath = ''; +foreach ($pathParts as $part) { + $accPath .= $part . '/'; + $breadcrumbsHTML .= ""; +} + +// Категории файлов для 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']; +?> + + + + + + S3 Файловый менеджер + + + + + + + + + +
+ $action_error
"; ?> + + +
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + __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'; } + ?> + + + + + + + + + + + + +
ИмяРазмерИзмененДействия
+ + + + -- +
+ + +
+
+ + + + + + + + + + + + + +
+ + +
+
Папка пуста
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/wd.php b/wd.php new file mode 100644 index 0000000..caf8cda --- /dev/null +++ b/wd.php @@ -0,0 +1,318 @@ +prepare("SELECT * FROM s3_mounts WHERE dav_user = ?"); +$stmt->execute([$dav_user]); +$mount = $stmt->fetch(); + +if (!$mount || !password_verify($dav_pass, $mount['dav_pass'])) { + sleep(2); + header('WWW-Authenticate: Basic realm="WebDAV S3 Gateway"'); + header('HTTP/1.0 401 Unauthorized'); + exit; +} + +// --- 2. Инициализация 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']; + +// --- 3. Парсинг путей --- +$baseUri = '/wd'; +$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); +// Вырезаем /wd из начала пути +$path = urldecode(substr($requestUri, strlen($baseUri))); +$path = ltrim($path, '/'); + +$method = $_SERVER['REQUEST_METHOD']; + +// Форматы дат для Windows WebDAV +function getDavDates($timestamp = null) { + if (!$timestamp) $timestamp = time(); + return [ + 'lastmod' => gmdate('D, d M Y H:i:s \G\M\T', $timestamp), // RFC 1123 + 'created' => gmdate('Y-m-d\TH:i:s\Z', $timestamp) // ISO 8601 + ]; +} + +// Генератор XML-узла для файла/папки (для идеальной совместимости с Windows) +function buildPropResponse($href, $isFolder, $size = 0, $lastModTime = null) { + $dates = getDavDates($lastModTime); + $href = implode('/', array_map('rawurlencode', explode('/', $href))); // Кодируем пути корректно + + $xml = "\n"; + $xml .= " {$href}\n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " {$dates['created']}\n"; + $xml .= " {$dates['lastmod']}\n"; + + if ($isFolder) { + $xml .= " \n"; + } else { + $xml .= " \n"; + $xml .= " {$size}\n"; + } + + // Фейковые блокировки, чтобы Windows разрешал запись + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + + $xml .= " \n"; + $xml .= " HTTP/1.1 200 OK\n"; + $xml .= " \n"; + $xml .= "\n"; + return $xml; +} + +// --- 4. Обработка методов --- +switch ($method) { + case 'OPTIONS': + header('Allow: OPTIONS, GET, HEAD, PUT, DELETE, MKCOL, PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK'); + header('DAV: 1, 2'); + header('MS-Author-Via: DAV'); + http_response_code(200); + break; + + case 'PROPFIND': + $depth = isset($_SERVER['HTTP_DEPTH']) ? $_SERVER['HTTP_DEPTH'] : '1'; + + $prefix = $path; + if ($prefix !== '' && substr($prefix, -1) !== '/') { + $prefix .= '/'; + } + + try { + $params = [ + 'Bucket' => $bucket, + 'Prefix' => $prefix === '/' ? '' : $prefix, + 'Delimiter' => '/' + ]; + + // Если запрашивают корень или папку с Depth: 0 (Windows делает это для проверки существования) + if ($depth == '0') { + $reqHref = $baseUri . '/' . $path; + if ($path === '' || substr($reqHref, -1) !== '/') $reqHref .= '/'; // Корректировка слешей для папок + + header('Content-Type: application/xml; charset="utf-8"'); + http_response_code(207); + echo ''."\n"; + echo ''."\n"; + echo buildPropResponse($reqHref, true); + echo "\n"; + exit; + } + + // Получаем список файлов из S3 + $result = $s3->listObjectsV2($params); + + header('Content-Type: application/xml; charset="utf-8"'); + http_response_code(207); + + echo ''."\n"; + echo ''."\n"; + + // 1. Текущая директория + $reqHref = $baseUri . '/' . $path; + if ($path !== '' && substr($reqHref, -1) !== '/') $reqHref .= '/'; + echo buildPropResponse($reqHref, true); + + // 2. Вложенные папки + if (isset($result['CommonPrefixes'])) { + foreach ($result['CommonPrefixes'] as $p) { + $folderHref = $baseUri . '/' . $p['Prefix']; + echo buildPropResponse($folderHref, true); + } + } + + // 3. Файлы + if (isset($result['Contents'])) { + foreach ($result['Contents'] as $c) { + if ($c['Key'] === $prefix) continue; // Пропускаем саму папку (она создается как пустой объект в S3) + $fileHref = $baseUri . '/' . $c['Key']; + $lastModTime = strtotime($c['LastModified']->__toString()); + echo buildPropResponse($fileHref, false, $c['Size'], $lastModTime); + } + } + + echo "\n"; + } catch (AwsException $e) { + http_response_code(500); + } + break; + + case 'GET': + case 'HEAD': + // ИСПРАВЛЕНИЕ ДЛЯ БРАУЗЕРА (Firefox): + // Если путь пустой или заканчивается на /, значит это папка. S3 не может скачать папку. + if ($path === '' || substr($path, -1) === '/') { + if ($method === 'GET') { + header('Content-Type: text/html; charset="utf-8"'); + echo ""; + echo "

Это эндпойнт WebDAV.

"; + echo "

Для просмотра файлов через браузер используйте веб-интерфейс:

"; + echo "Перейти в UI"; + echo ""; + } + exit; + } + + try { + $params = ['Bucket' => $bucket, 'Key' => $path]; + + if (isset($_SERVER['HTTP_RANGE'])) { + $params['Range'] = $_SERVER['HTTP_RANGE']; + } + + $object = $s3->getObject($params); + + header('Content-Type: ' . $object['ContentType']); + header('Content-Length: ' . $object['ContentLength']); + header('Accept-Ranges: bytes'); + + // Заголовки кеширования, которые любит Windows + header('Cache-Control: no-cache'); + header('Pragma: no-cache'); + + if (isset($_SERVER['HTTP_RANGE']) && isset($object['ContentRange'])) { + http_response_code(206); + header('Content-Range: ' . $object['ContentRange']); + } else { + http_response_code(200); + } + + if ($method === 'GET') { + echo $object['Body']; + } + } catch (AwsException $e) { + if ($e->getAwsErrorCode() === 'NoSuchKey') { + http_response_code(404); + } else { + http_response_code(500); + } + } + break; + + case 'PUT': + // Снимаем лимит времени выполнения скрипта для загрузки больших файлов + set_time_limit(0); + ignore_user_abort(true); + + $tempFile = tempnam(sys_get_temp_dir(), 'dav_put_'); + + try { + // 1. Сохраняем поток WebDAV во временный локальный файл + $input = fopen('php://input', 'r'); + $output = fopen($tempFile, 'w'); + + // stream_copy_to_stream не забивает оперативную память, а перекачивает данные напрямую + stream_copy_to_stream($input, $output); + + fclose($input); + fclose($output); + + // 2. Отправляем физический файл в S3 + // Используем 'SourceFile' вместо 'Body', это включает Multipart-загрузку S3 под капотом + $s3->putObject([ + 'Bucket' => $bucket, + 'Key' => $path, + 'SourceFile' => $tempFile + ]); + + // 3. Удаляем временный файл + if (file_exists($tempFile)) { + unlink($tempFile); + } + + http_response_code(201); // Created + } catch (Exception $e) { + // Обязательно удаляем мусор, если произошла ошибка + if (file_exists($tempFile)) { + unlink($tempFile); + } + http_response_code(500); + } + break; + + case 'DELETE': + try { + // Для папок в S3 нужно удалять все вложенные объекты + if (substr($path, -1) === '/') { + $objects = $s3->listObjectsV2(['Bucket' => $bucket, 'Prefix' => $path]); + if (isset($objects['Contents'])) { + $deleteList = []; + foreach ($objects['Contents'] as $obj) { + $deleteList[] = ['Key' => $obj['Key']]; + } + $s3->deleteObjects([ + 'Bucket' => $bucket, + 'Delete' => ['Objects' => $deleteList] + ]); + } + } else { + $s3->deleteObject(['Bucket' => $bucket, 'Key' => $path]); + } + http_response_code(204); + } catch (AwsException $e) { + http_response_code(500); + } + break; + + case 'MKCOL': + if (substr($path, -1) !== '/') $path .= '/'; + try { + $s3->putObject(['Bucket' => $bucket, 'Key' => $path, 'Body' => '']); + http_response_code(201); + } catch (AwsException $e) { + http_response_code(500); + } + break; + + case 'LOCK': + $token = 'urn:uuid:' . sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); + header('Lock-Token: <' . $token . '>'); + header('Content-Type: application/xml; charset="utf-8"'); + echo "\nInfinity$token"; + http_response_code(200); + break; + + case 'UNLOCK': + case 'PROPPATCH': // Пропускаем запрос изменения свойств от Windows + http_response_code(204); + break; + + default: + http_response_code(405); + break; +} \ No newline at end of file