flysystem-offload/src/Filesystem/Adapters/WebdavAdapter.php

509 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace FlysystemOffload\Filesystem\Adapters;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\Config;
use League\Flysystem\FileAttributes;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\UnableToReadFile;
use League\Flysystem\UnableToWriteFile;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToCreateDirectory;
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\Visibility;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use Sabre\DAV\Client;
class WebdavAdapter implements FilesystemAdapter
{
private Client $client;
private string $prefix;
private PortableVisibilityConverter $visibilityConverter;
private bool $baseDirectoryEnsured = false;
public function __construct(
Client $client,
string $prefix = '',
?PortableVisibilityConverter $visibilityConverter = null
) {
$this->client = $client;
$this->prefix = trim($prefix, '/');
$this->visibilityConverter = $visibilityConverter ?? new PortableVisibilityConverter();
error_log(sprintf(
'[WebdavAdapter] Initialized with prefix: "%s"',
$this->prefix
));
}
/**
* Asegurar que el directorio base (prefix) existe
* Se ejecuta lazy (solo cuando se necesita)
*/
private function ensureBaseDirectoryExists(): void
{
if ($this->baseDirectoryEnsured || $this->prefix === '') {
return;
}
error_log('[WebdavAdapter] Ensuring base directory exists...');
$parts = array_filter(explode('/', $this->prefix));
$path = '';
foreach ($parts as $part) {
$path .= '/' . $part;
try {
// Intentar verificar si existe
$this->client->propFind($path, ['{DAV:}resourcetype'], 0);
error_log(sprintf('[WebdavAdapter] Base directory exists: "%s"', $path));
} catch (\Exception $e) {
// No existe, crear
error_log(sprintf('[WebdavAdapter] Base directory does not exist, creating: "%s"', $path));
try {
$response = $this->client->request('MKCOL', $path);
// Verificar el código de estado
$statusCode = $response['statusCode'] ?? 0;
if ($statusCode >= 200 && $statusCode < 300) {
// Éxito (201 Created)
error_log(sprintf('[WebdavAdapter] Created base directory: "%s", status: %d', $path, $statusCode));
} elseif ($statusCode === 405) {
// 405 Method Not Allowed = ya existe
error_log(sprintf('[WebdavAdapter] Base directory already exists: "%s"', $path));
} else {
// Error
$errorMsg = sprintf('Failed to create directory "%s", status: %d', $path, $statusCode);
error_log(sprintf('[WebdavAdapter] %s', $errorMsg));
throw new \RuntimeException($errorMsg);
}
} catch (\Exception $e2) {
// Verificar si el error es porque ya existe (405)
if (strpos($e2->getMessage(), '405') !== false) {
error_log(sprintf('[WebdavAdapter] Base directory already exists (405): "%s"', $path));
} else {
error_log(sprintf(
'[WebdavAdapter] Failed to create base directory: "%s", error: %s',
$path,
$e2->getMessage()
));
throw $e2;
}
}
}
}
$this->baseDirectoryEnsured = true;
error_log('[WebdavAdapter] Base directory ensured successfully');
}
private function prefixPath(string $path): string
{
if ($this->prefix === '') {
$prefixed = '/' . ltrim($path, '/');
} else {
$prefixed = '/' . $this->prefix . '/' . ltrim($path, '/');
}
error_log(sprintf(
'[WebdavAdapter] prefixPath - input: "%s", output: "%s"',
$path,
$prefixed
));
return $prefixed;
}
public function fileExists(string $path): bool
{
try {
$response = $this->client->propFind($this->prefixPath($path), ['{DAV:}resourcetype'], 0);
$exists = !empty($response);
error_log(sprintf(
'[WebdavAdapter] fileExists - path: "%s", exists: %s',
$path,
$exists ? 'true' : 'false'
));
return $exists;
} catch (\Exception $e) {
error_log(sprintf(
'[WebdavAdapter] fileExists error - path: "%s", error: %s',
$path,
$e->getMessage()
));
return false;
}
}
public function directoryExists(string $path): bool
{
return $this->fileExists($path);
}
public function write(string $path, string $contents, Config $config): void
{
// ✅ CRÍTICO: Asegurar que el directorio base existe ANTES de escribir
$this->ensureBaseDirectoryExists();
$prefixedPath = $this->prefixPath($path);
error_log(sprintf(
'[WebdavAdapter] write - path: "%s", prefixed: "%s", size: %d bytes',
$path,
$prefixedPath,
strlen($contents)
));
// Asegurar que el directorio padre del archivo existe
$dirname = dirname($path);
if ($dirname !== '.' && $dirname !== '') {
$this->ensureDirectoryExists($dirname, $config);
}
try {
$response = $this->client->request('PUT', $prefixedPath, $contents);
if ($response['statusCode'] >= 400) {
throw UnableToWriteFile::atLocation(
$path,
"WebDAV returned status {$response['statusCode']}"
);
}
error_log(sprintf(
'[WebdavAdapter] write success - path: "%s", status: %d',
$path,
$response['statusCode']
));
} catch (\Exception $e) {
error_log(sprintf(
'[WebdavAdapter] write error - path: "%s", error: %s',
$path,
$e->getMessage()
));
throw UnableToWriteFile::atLocation($path, $e->getMessage(), $e);
}
}
public function writeStream(string $path, $contents, Config $config): void
{
$streamContents = stream_get_contents($contents);
if ($streamContents === false) {
throw UnableToWriteFile::atLocation($path, 'Unable to read from stream');
}
$this->write($path, $streamContents, $config);
}
public function read(string $path): string
{
try {
$response = $this->client->request('GET', $this->prefixPath($path));
if ($response['statusCode'] >= 400) {
throw UnableToReadFile::fromLocation(
$path,
"WebDAV returned status {$response['statusCode']}"
);
}
return $response['body'];
} catch (\Exception $e) {
throw UnableToReadFile::fromLocation($path, $e->getMessage(), $e);
}
}
public function readStream(string $path)
{
$resource = fopen('php://temp', 'r+');
if ($resource === false) {
throw UnableToReadFile::fromLocation($path, 'Unable to create temp stream');
}
fwrite($resource, $this->read($path));
rewind($resource);
return $resource;
}
public function delete(string $path): void
{
try {
$response = $this->client->request('DELETE', $this->prefixPath($path));
if ($response['statusCode'] >= 400 && $response['statusCode'] !== 404) {
throw UnableToDeleteFile::atLocation(
$path,
"WebDAV returned status {$response['statusCode']}"
);
}
error_log(sprintf(
'[WebdavAdapter] delete success - path: "%s"',
$path
));
} catch (\Exception $e) {
error_log(sprintf(
'[WebdavAdapter] delete error - path: "%s", error: %s',
$path,
$e->getMessage()
));
throw UnableToDeleteFile::atLocation($path, $e->getMessage(), $e);
}
}
public function deleteDirectory(string $path): void
{
$this->delete($path);
}
public function createDirectory(string $path, Config $config): void
{
// ✅ Asegurar que el directorio base existe
$this->ensureBaseDirectoryExists();
$prefixedPath = $this->prefixPath($path);
error_log(sprintf(
'[WebdavAdapter] createDirectory - path: "%s", prefixed: "%s"',
$path,
$prefixedPath
));
try {
$response = $this->client->request('MKCOL', $prefixedPath);
// 405 significa que el directorio ya existe
if ($response['statusCode'] >= 400 && $response['statusCode'] !== 405) {
throw UnableToCreateDirectory::atLocation(
$path,
"WebDAV returned status {$response['statusCode']}"
);
}
error_log(sprintf(
'[WebdavAdapter] createDirectory success - path: "%s", status: %d',
$path,
$response['statusCode']
));
} catch (\Exception $e) {
error_log(sprintf(
'[WebdavAdapter] createDirectory error - path: "%s", error: %s',
$path,
$e->getMessage()
));
// 405 significa que ya existe, no es un error
if (strpos($e->getMessage(), '405') === false) {
throw UnableToCreateDirectory::atLocation($path, $e->getMessage(), $e);
} else {
error_log(sprintf('[WebdavAdapter] Directory already exists: "%s"', $path));
}
}
}
private function ensureDirectoryExists(string $dirname, Config $config): void
{
if ($dirname === '' || $dirname === '.') {
return;
}
error_log(sprintf('[WebdavAdapter] ensureDirectoryExists - dirname: "%s"', $dirname));
$parts = array_filter(explode('/', trim($dirname, '/')));
$path = '';
foreach ($parts as $part) {
$path .= ($path !== '' ? '/' : '') . $part;
error_log(sprintf('[WebdavAdapter] Checking/creating directory: "%s"', $path));
// Intentar crear directamente
try {
$this->createDirectory($path, $config);
} catch (UnableToCreateDirectory $e) {
// Si falla y no es porque ya existe, propagar el error
if (strpos($e->getMessage(), '405') === false && !$this->directoryExists($path)) {
error_log(sprintf(
'[WebdavAdapter] Failed to ensure directory exists: "%s", error: %s',
$path,
$e->getMessage()
));
throw $e;
}
}
}
}
public function setVisibility(string $path, string $visibility): void
{
// WebDAV no soporta visibilidad de forma nativa
}
public function visibility(string $path): FileAttributes
{
return new FileAttributes($path, null, Visibility::PUBLIC);
}
public function mimeType(string $path): FileAttributes
{
try {
$response = $this->client->propFind(
$this->prefixPath($path),
['{DAV:}getcontenttype'],
0
);
$mimeType = $response['{DAV:}getcontenttype'] ?? null;
return new FileAttributes($path, null, null, null, $mimeType);
} catch (\Exception $e) {
throw UnableToRetrieveMetadata::mimeType($path, $e->getMessage(), $e);
}
}
public function lastModified(string $path): FileAttributes
{
try {
$response = $this->client->propFind(
$this->prefixPath($path),
['{DAV:}getlastmodified'],
0
);
$lastModified = $response['{DAV:}getlastmodified'] ?? null;
$timestamp = $lastModified ? strtotime($lastModified) : null;
return new FileAttributes($path, null, null, $timestamp);
} catch (\Exception $e) {
throw UnableToRetrieveMetadata::lastModified($path, $e->getMessage(), $e);
}
}
public function fileSize(string $path): FileAttributes
{
try {
$response = $this->client->propFind(
$this->prefixPath($path),
['{DAV:}getcontentlength'],
0
);
$size = isset($response['{DAV:}getcontentlength'])
? (int) $response['{DAV:}getcontentlength']
: null;
return new FileAttributes($path, $size);
} catch (\Exception $e) {
throw UnableToRetrieveMetadata::fileSize($path, $e->getMessage(), $e);
}
}
public function listContents(string $path, bool $deep): iterable
{
try {
$response = $this->client->propFind(
$this->prefixPath($path),
[
'{DAV:}resourcetype',
'{DAV:}getcontentlength',
'{DAV:}getlastmodified',
'{DAV:}getcontenttype',
],
$deep ? \Sabre\DAV\Client::DEPTH_INFINITY : 1
);
foreach ($response as $itemPath => $properties) {
$relativePath = $this->removePrefix($itemPath);
if ($relativePath === $path || $relativePath === '') {
continue;
}
$isDirectory = isset($properties['{DAV:}resourcetype'])
&& strpos($properties['{DAV:}resourcetype']->serialize(new \Sabre\Xml\Writer()), 'collection') !== false;
if ($isDirectory) {
yield new DirectoryAttributes($relativePath);
} else {
$size = isset($properties['{DAV:}getcontentlength'])
? (int) $properties['{DAV:}getcontentlength']
: null;
$lastModified = isset($properties['{DAV:}getlastmodified'])
? strtotime($properties['{DAV:}getlastmodified'])
: null;
$mimeType = $properties['{DAV:}getcontenttype'] ?? null;
yield new FileAttributes(
$relativePath,
$size,
null,
$lastModified,
$mimeType
);
}
}
} catch (\Exception $e) {
error_log(sprintf(
'[WebdavAdapter] listContents error - path: "%s", error: %s',
$path,
$e->getMessage()
));
}
}
private function removePrefix(string $path): string
{
$path = '/' . trim($path, '/');
if ($this->prefix !== '') {
$prefixWithSlash = '/' . $this->prefix . '/';
if (str_starts_with($path, $prefixWithSlash)) {
return substr($path, strlen($prefixWithSlash));
}
}
return ltrim($path, '/');
}
public function move(string $source, string $destination, Config $config): void
{
$this->ensureBaseDirectoryExists();
try {
$this->client->request(
'MOVE',
$this->prefixPath($source),
null,
['Destination' => $this->prefixPath($destination)]
);
} catch (\Exception $e) {
throw new \RuntimeException("Unable to move file from {$source} to {$destination}: " . $e->getMessage(), 0, $e);
}
}
public function copy(string $source, string $destination, Config $config): void
{
$this->ensureBaseDirectoryExists();
try {
$this->client->request(
'COPY',
$this->prefixPath($source),
null,
['Destination' => $this->prefixPath($destination)]
);
} catch (\Exception $e) {
throw new \RuntimeException("Unable to copy file from {$source} to {$destination}: " . $e->getMessage(), 0, $e);
}
}
}