basePath); $this->permissions = new PermissionChecker($pathProvider); $this->fiberManager = $fiberManager ?? $this->createDefaultFiberManager(); $this->validator = $validator; $this->logger = $logger; } private function createDefaultFiberManager(): FiberManager { // Create minimal dependencies for FiberManager $clock = new SystemClock(); $timer = new SystemTimer($clock); return new FiberManager($clock, $timer); } private function resolvePath(string $path): string { // Absolute path bleibt unverändert if (str_starts_with($path, '/')) { return $path; } // Relative paths werden an basePath gehängt return rtrim($this->basePath, '/') . '/' . ltrim($path, '/'); } /** * Log FileOperation context wenn Logger verfügbar */ private function logOperation(FileOperationContext $context): void { if ($this->logger === null) { return; } $logContext = LogContext::fromArray($context->toArray()); if ($context->isHighSeverity()) { $this->logger->framework->warning($context->toString(), $logContext); } elseif ($context->isLargeOperation()) { $this->logger->framework->info($context->toString(), $logContext); } else { $this->logger->framework->debug($context->toString(), $logContext); } } public function get(string $path): string { $resolvedPath = $this->resolvePath($path); // Validate with FileValidator if available if ($this->validator !== null) { $this->validator->validateRead($resolvedPath); } if (! is_file($resolvedPath)) { throw new FileNotFoundException($path); } if (! is_readable($resolvedPath)) { throw FilePermissionException::read($path, 'File is not readable', $this->permissions); } $content = @file_get_contents($resolvedPath); if ($content === false) { $error = error_get_last(); if ($error && str_contains($error['message'], 'Permission denied')) { throw FilePermissionException::read($path, $error['message'], $this->permissions); } throw new FileReadException($path); } // Log successful read operation $this->logOperation(FileOperationContext::forRead( path: $resolvedPath, bytesRead: FileSize::fromBytes(strlen($content)) )); return $content; } public function put(string $path, string $content): void { $resolvedPath = $this->resolvePath($path); $dir = dirname($resolvedPath); // Validate with FileValidator if available if ($this->validator !== null) { $fileSize = FileSize::fromBytes(strlen($content)); $this->validator->validateWrite($resolvedPath, $fileSize); } // Clear stat cache before directory operations to ensure fresh info clearstatcache(true, $dir); // Prüfe Directory-Permissions if (! is_dir($dir)) { if (! @mkdir($dir, 0777, true) && ! is_dir($dir)) { $error = error_get_last(); if ($error && str_contains($error['message'], 'Permission denied')) { throw FilePermissionException::createDirectory($dir, $error['message'], $this->permissions); } throw new DirectoryCreateException($dir); } } elseif (! is_writable($dir)) { throw FilePermissionException::write($path, 'Directory is not writable: ' . $dir, $this->permissions); } // Prüfe File-Permissions wenn Datei bereits existiert if (is_file($resolvedPath) && ! is_writable($resolvedPath)) { throw FilePermissionException::write($path, 'File is not writable', $this->permissions); } if (@file_put_contents($resolvedPath, $content) === false) { $error = error_get_last(); if ($error && str_contains($error['message'], 'Permission denied')) { throw FilePermissionException::write($path, $error['message'], $this->permissions); } throw new FileWriteException($path); } // Set readable permissions for files (0644 = owner rw, group r, others r) // This ensures cache files and other storage files are always readable $this->permissions->setPermissions( $path, ValueObjects\FilePermissions::readWriteOwnerReadAll() ); // Log successful write operation $this->logOperation(FileOperationContext::forWrite( path: $resolvedPath, bytesWritten: FileSize::fromBytes(strlen($content)) )); } public function exists(string $path): bool { return is_file($this->resolvePath($path)); } public function delete(string $path): void { $resolvedPath = $this->resolvePath($path); // Validate with FileValidator if available if ($this->validator !== null) { $this->validator->validatePath($resolvedPath); $this->validator->validateExists($resolvedPath); } if (! is_file($resolvedPath)) { throw new FileNotFoundException($path); } // Prüfe Directory-Permissions // WICHTIG: Für unlink() braucht man nur Schreibrechte im Verzeichnis, nicht auf der Datei selbst! $dir = dirname($resolvedPath); if (! is_writable($dir)) { throw FilePermissionException::delete($path, 'Directory is not writable: ' . $dir); } // ENTFERNT: Prüfe File-Permissions // Für unlink() benötigt man nur Schreibrechte im Parent-Verzeichnis, nicht auf der Datei selbst. // Dies ist ein Unix-Standard-Verhalten: unlink() entfernt einen Directory-Eintrag, nicht die Datei selbst. // if (! is_writable($resolvedPath)) { // throw FilePermissionException::delete($path, 'File is not writable'); // } if (! @unlink($resolvedPath)) { $error = error_get_last(); if ($error && str_contains($error['message'], 'Permission denied')) { throw FilePermissionException::delete($path, $error['message'], $this->permissions); } throw new FileDeleteException($path); } // Log successful delete operation $this->logOperation(FileOperationContext::forOperation( operation: FileOperation::DELETE, path: $resolvedPath )); } public function copy(string $source, string $destination): void { $resolvedSource = $this->resolvePath($source); $resolvedDestination = $this->resolvePath($destination); // Validate source with FileValidator if available if ($this->validator !== null) { $this->validator->validateRead($resolvedSource); } // Validate destination with FileValidator if available if ($this->validator !== null) { $this->validator->validatePath($resolvedDestination); $this->validator->validateExtension($resolvedDestination); } if (! is_file($resolvedSource)) { throw new FileNotFoundException($source); } // Stelle sicher, dass das Zielverzeichnis existiert $dir = dirname($resolvedDestination); if (! is_dir($dir)) { $this->createDirectory($destination); } if (! @copy($resolvedSource, $resolvedDestination)) { throw new FileCopyException($source, $destination); } // Log successful copy operation $this->logOperation(FileOperationContext::forOperationWithDestination( operation: FileOperation::COPY, sourcePath: $resolvedSource, destinationPath: $resolvedDestination )); } public function size(string $path): int { $resolvedPath = $this->resolvePath($path); if (! is_file($resolvedPath)) { throw new FileNotFoundException($path); } $size = @filesize($resolvedPath); if ($size === false) { throw new FileReadException($path); } return $size; } public function lastModified(string $path): int { $resolvedPath = $this->resolvePath($path); if (! is_file($resolvedPath)) { throw new FileNotFoundException($path); } $time = @filemtime($resolvedPath); if ($time === false) { throw new FileReadException($path); } return $time; } public function getMimeType(string $path): string { $resolvedPath = $this->resolvePath($path); if (! is_file($resolvedPath)) { throw new FileNotFoundException($path); } $mimeType = @mime_content_type($resolvedPath); if ($mimeType === false) { throw new FileMetadataException($path); } return $mimeType; } public function isReadable(string $path): bool { $resolvedPath = $this->resolvePath($path); if (! is_file($resolvedPath)) { throw new FileNotFoundException($path); } return is_readable($resolvedPath); } public function isWritable(string $path): bool { $resolvedPath = $this->resolvePath($path); if (! is_file($resolvedPath)) { throw new FileNotFoundException($path); } return is_writable($resolvedPath); } public function listDirectory(string $directory): array { $resolvedDirectory = $this->resolvePath($directory); if (! is_dir($resolvedDirectory)) { throw new DirectoryCreateException($directory); } $files = @scandir($resolvedDirectory); if ($files === false) { throw new DirectoryListException($directory); } // Entferne . und .. Einträge $files = array_filter($files, function ($file) { return $file !== '.' && $file !== '..'; }); // Relative Pfade zurückgeben (ohne basePath) $result = []; foreach ($files as $file) { $fullPath = $resolvedDirectory . DIRECTORY_SEPARATOR . $file; // Entferne basePath für relativen Pfad $relativePath = str_starts_with($fullPath, $this->basePath) ? substr($fullPath, strlen(rtrim($this->basePath, '/')) + 1) : $fullPath; $result[] = $relativePath; } return array_values($result); } public function createDirectory(string $path, int $permissions = 0755, bool $recursive = true): void { $resolvedPath = $this->resolvePath($path); if (is_dir($resolvedPath)) { return; // Verzeichnis existiert bereits } // Prüfe Parent-Directory Permissions $parentDir = dirname($resolvedPath); if (is_dir($parentDir) && ! is_writable($parentDir)) { throw FilePermissionException::createDirectory($path, 'Parent directory is not writable: ' . $parentDir, $this->permissions); } if (! @mkdir($resolvedPath, $permissions, $recursive) && ! is_dir($resolvedPath)) { $error = error_get_last(); if ($error && str_contains($error['message'], 'Permission denied')) { throw FilePermissionException::createDirectory($path, $error['message'], $this->permissions); } throw new DirectoryCreateException($path); } } // Batch-Operationen für explizite Parallelverarbeitung public function batch(array $operations): array { return $this->fiberManager->batch($operations); } public function getMultiple(array $paths): array { $operations = []; foreach ($paths as $path) { $operations[$path] = fn () => $this->get($path); } return $this->batch($operations); } public function putMultiple(array $files): void { $operations = []; foreach ($files as $path => $content) { $operations[$path] = fn () => $this->put($path, $content); } $this->batch($operations); } public function getMetadataMultiple(array $paths): array { $operations = []; foreach ($paths as $path) { $operations[$path] = fn () => new FileMetadata( size: $this->size($path), lastModified: $this->lastModified($path), mimeType: $this->getMimeType($path), isReadable: $this->isReadable($path), isWritable: $this->isWritable($path) ); } return $this->batch($operations); } }