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