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 User)
+
+
+
+
+
Подключенные S3
+
+ WebDAV Логин Bucket Endpoint Действия
+
+
+ = htmlspecialchars($m['dav_user']) ?>
+ = htmlspecialchars($m['s3_bucket']) ?>
+ = htmlspecialchars($m['s3_endpoint']) ?>
+
+ Удалить
+
+
+
+
+
+
+
\ 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 .= "" . htmlspecialchars($part) . " ";
+}
+
+// Категории файлов для 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
"; ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ Имя
+ Размер
+ Изменен
+ Действия
+
+
+
+
+
+
+
+
+ = htmlspecialchars($folderName) ?>
+
+
+ -
+ -
+
+
+
+
+
+
+
+ __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'; }
+ ?>
+
+
+
+
+
+ = htmlspecialchars($fileName) ?>
+
+
+ = htmlspecialchars($fileName) ?>
+
+
+ = $size ?>
+ = $date ?>
+
+
+
+
+
+
+
+
+
+
+
+ Папка пуста
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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