509 lines
17 KiB
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);
|
|
}
|
|
}
|
|
}
|