1.0.0
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
if (file_exists('aws.phar')) {
|
||||
require 'aws.phar';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
die("Требуется aws.phar");
|
||||
}
|
||||
|
||||
use Aws\S3\S3Client;
|
||||
use Aws\Exception\AwsException;
|
||||
|
||||
$db_file = __DIR__ . '/datas.db';
|
||||
$db = new PDO('sqlite:' . $db_file);
|
||||
|
||||
// --- 1. Авторизация ---
|
||||
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
|
||||
header('WWW-Authenticate: Basic realm="WebDAV S3 Gateway"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
exit;
|
||||
}
|
||||
|
||||
$dav_user = $_SERVER['PHP_AUTH_USER'];
|
||||
$dav_pass = $_SERVER['PHP_AUTH_PW'];
|
||||
|
||||
$stmt = $db->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 = "<D:response>\n";
|
||||
$xml .= " <D:href>{$href}</D:href>\n";
|
||||
$xml .= " <D:propstat>\n";
|
||||
$xml .= " <D:prop>\n";
|
||||
$xml .= " <D:creationdate>{$dates['created']}</D:creationdate>\n";
|
||||
$xml .= " <D:getlastmodified>{$dates['lastmod']}</D:getlastmodified>\n";
|
||||
|
||||
if ($isFolder) {
|
||||
$xml .= " <D:resourcetype><D:collection/></D:resourcetype>\n";
|
||||
} else {
|
||||
$xml .= " <D:resourcetype/>\n";
|
||||
$xml .= " <D:getcontentlength>{$size}</D:getcontentlength>\n";
|
||||
}
|
||||
|
||||
// Фейковые блокировки, чтобы Windows разрешал запись
|
||||
$xml .= " <D:supportedlock>\n";
|
||||
$xml .= " <D:lockentry><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>\n";
|
||||
$xml .= " <D:lockentry><D:lockscope><D:shared/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>\n";
|
||||
$xml .= " </D:supportedlock>\n";
|
||||
|
||||
$xml .= " </D:prop>\n";
|
||||
$xml .= " <D:status>HTTP/1.1 200 OK</D:status>\n";
|
||||
$xml .= " </D:propstat>\n";
|
||||
$xml .= "</D:response>\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 '<?xml version="1.0" encoding="utf-8"?>'."\n";
|
||||
echo '<D:multistatus xmlns:D="DAV:">'."\n";
|
||||
echo buildPropResponse($reqHref, true);
|
||||
echo "</D:multistatus>\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Получаем список файлов из S3
|
||||
$result = $s3->listObjectsV2($params);
|
||||
|
||||
header('Content-Type: application/xml; charset="utf-8"');
|
||||
http_response_code(207);
|
||||
|
||||
echo '<?xml version="1.0" encoding="utf-8"?>'."\n";
|
||||
echo '<D:multistatus xmlns:D="DAV:">'."\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 "</D:multistatus>\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 "<!DOCTYPE html><html><body style='background:#1c1b1f;color:white;font-family:sans-serif;text-align:center;padding:50px;'>";
|
||||
echo "<h2>Это эндпойнт WebDAV.</h2>";
|
||||
echo "<p>Для просмотра файлов через браузер используйте веб-интерфейс:</p>";
|
||||
echo "<a href='/ui' style='background:#d0bcff;color:#381e72;padding:10px 20px;border-radius:20px;text-decoration:none;font-weight:bold;display:inline-block;margin-top:20px;'>Перейти в UI</a>";
|
||||
echo "</body></html>";
|
||||
}
|
||||
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 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:prop xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock><D:locktype><D:write/></D:locktype><D:lockscope><D:exclusive/></D:lockscope><D:depth>Infinity</D:depth><D:locktoken><D:href>$token</D:href></D:locktoken></D:activelock></D:lockdiscovery></D:prop>";
|
||||
http_response_code(200);
|
||||
break;
|
||||
|
||||
case 'UNLOCK':
|
||||
case 'PROPPATCH': // Пропускаем запрос изменения свойств от Windows
|
||||
http_response_code(204);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
break;
|
||||
}
|
||||
Reference in New Issue
Block a user