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; }