*/ private array $directoryListing = []; private int $directoryPosition = 0; public static function register( FilesystemOperator $filesystem, string $protocol = 'flysystem', string $rootPrefix = '', string $defaultVisibility = Visibility::PUBLIC, bool $force = true ): void { self::$filesystem = $filesystem; self::$protocol = $protocol; self::$rootPrefix = PathHelper::normalize($rootPrefix); self::$defaultVisibility = $defaultVisibility; $wrappers = stream_get_wrappers(); if (in_array($protocol, $wrappers, true)) { if (! $force) { return; } stream_wrapper_unregister($protocol); } if (! stream_wrapper_register($protocol, static::class, STREAM_IS_URL)) { throw new \RuntimeException(sprintf('Unable to register stream wrapper for protocol "%s".', $protocol)); } } public static function unregister(): void { if (self::$protocol !== '' && in_array(self::$protocol, stream_get_wrappers(), true)) { stream_wrapper_unregister(self::$protocol); } self::$filesystem = null; } public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool { $this->ensureFilesystem(); $this->uri = $path; $this->path = self::uriToKey($path); $this->mode = $mode; $this->resource = fopen('php://temp', 'w+b'); $this->dirty = false; if ($this->resource === false) { return false; } if ($this->requiresExistingFile($mode) && ! $this->fileExists($this->path)) { if ($options & STREAM_REPORT_ERRORS) { trigger_error(sprintf('Flysystem path "%s" does not exist.', $this->path), E_USER_WARNING); } return false; } if ($this->isExclusiveCreateMode($mode) && $this->fileExists($this->path)) { return false; } if (($this->requiresExistingFile($mode) || $this->isAppendMode($mode)) && $this->fileExists($this->path)) { try { $contents = self::$filesystem->read($this->path); fwrite($this->resource, $contents); unset($contents); if ($this->isAppendMode($mode)) { fseek($this->resource, 0, SEEK_END); } else { rewind($this->resource); } } catch (UnableToReadFile|FilesystemException $exception) { if ($options & STREAM_REPORT_ERRORS) { trigger_error($exception->getMessage(), E_USER_WARNING); } return false; } } return true; } public function stream_read(int $count): string|false { if (! $this->resource) { return false; } return fread($this->resource, $count); } public function stream_write(string $data): int|false { if (! $this->resource) { return false; } $written = fwrite($this->resource, $data); if ($written !== false) { $this->dirty = true; } return $written; } public function stream_tell(): int|false { if (! $this->resource) { return false; } return ftell($this->resource); } public function stream_eof(): bool { if (! $this->resource) { return true; } return feof($this->resource); } public function stream_seek(int $offset, int $whence = SEEK_SET): bool { if (! $this->resource) { return false; } return fseek($this->resource, $offset, $whence) === 0; } public function stream_flush(): bool { if (! $this->resource || ! $this->dirty || ! $this->isWriteMode($this->mode)) { return true; } $position = ftell($this->resource); rewind($this->resource); $contents = stream_get_contents($this->resource); fseek($this->resource, $position); try { if ($this->fileExists($this->path)) { try { self::$filesystem->delete($this->path); } catch (UnableToDeleteFile|FilesystemException) { // Intentamos sobrescribir igualmente. } } self::$filesystem->write( $this->path, (string) $contents, ['visibility' => self::$defaultVisibility] ); $this->dirty = false; return true; } catch (UnableToWriteFile|FilesystemException $exception) { trigger_error($exception->getMessage(), E_USER_WARNING); return false; } } public function stream_close(): void { $this->stream_flush(); if ($this->resource) { fclose($this->resource); } $this->resource = null; $this->mode = 'r'; $this->path = ''; $this->uri = ''; $this->dirty = false; } public function stream_truncate(int $new_size): bool { if (! $this->resource) { return false; } $result = ftruncate($this->resource, $new_size); if ($result) { $this->dirty = true; } return $result; } public function stream_stat(): array|false { if ($this->path === '') { return false; } return $this->statKey($this->path); } public function url_stat(string $path, int $flags): array|false { $key = self::uriToKey($path); $stat = $this->statKey($key); if ($stat === false && ($flags & STREAM_URL_STAT_QUIET) === 0) { trigger_error(sprintf('Flysystem path "%s" not found.', $key), E_USER_WARNING); } return $stat; } public function unlink(string $path): bool { $this->ensureFilesystem(); $key = self::uriToKey($path); try { if ($this->fileExists($key)) { self::$filesystem->delete($key); } return true; } catch (UnableToDeleteFile|FilesystemException $exception) { trigger_error($exception->getMessage(), E_USER_WARNING); return false; } } public function rename(string $path_from, string $path_to): bool { $this->ensureFilesystem(); $from = self::uriToKey($path_from); $to = self::uriToKey($path_to); try { self::$filesystem->move($from, $to); return true; } catch (UnableToMoveFile|FilesystemException $exception) { trigger_error($exception->getMessage(), E_USER_WARNING); return false; } } public function mkdir(string $path, int $mode, int $options): bool { $this->ensureFilesystem(); $key = self::uriToKey($path); try { self::$filesystem->createDirectory($key, ['visibility' => self::$defaultVisibility]); return true; } catch (FilesystemException $exception) { if ($options & STREAM_REPORT_ERRORS) { trigger_error($exception->getMessage(), E_USER_WARNING); } return false; } } public function rmdir(string $path, int $options): bool { $this->ensureFilesystem(); $key = self::uriToKey($path); try { self::$filesystem->deleteDirectory($key); return true; } catch (UnableToDeleteDirectory|FilesystemException $exception) { if ($options & STREAM_REPORT_ERRORS) { trigger_error($exception->getMessage(), E_USER_WARNING); } return false; } } public function dir_opendir(string $path, int $options): bool { $this->ensureFilesystem(); $key = self::uriToKey($path); $this->directoryListing = ['.', '..']; try { foreach (self::$filesystem->listContents($key, false) as $attributes) { if ($attributes instanceof StorageAttributes) { $this->directoryListing[] = basename($attributes->path()); } } } catch (FilesystemException $exception) { if ($options & STREAM_REPORT_ERRORS) { trigger_error($exception->getMessage(), E_USER_WARNING); } return false; } $this->directoryPosition = 0; return true; } public function dir_readdir(): string|false { if (! isset($this->directoryListing[$this->directoryPosition])) { return false; } return $this->directoryListing[$this->directoryPosition++]; } public function dir_rewinddir(): bool { $this->directoryPosition = 0; return true; } public function dir_closedir(): bool { $this->directoryListing = []; $this->directoryPosition = 0; return true; } public function stream_metadata(string $path, int $option, mixed $value): bool { $this->ensureFilesystem(); if ($option === STREAM_META_TOUCH) { $key = self::uriToKey($path); try { if (! $this->fileExists($key)) { self::$filesystem->write($key, '', ['visibility' => self::$defaultVisibility]); } return true; } catch (FilesystemException $exception) { trigger_error($exception->getMessage(), E_USER_WARNING); return false; } } return false; } public function stream_set_option(int $option, int $arg1, int $arg2): bool { return false; } public function stream_lock(int $operation): bool { return true; } private function requiresExistingFile(string $mode): bool { return str_starts_with($mode, 'r'); } private function isExclusiveCreateMode(string $mode): bool { return str_starts_with($mode, 'x'); } private function isAppendMode(string $mode): bool { return str_contains($mode, 'a'); } private function isWriteMode(string $mode): bool { return strpbrk($mode, 'waxc+') !== false; } private static function uriToKey(string $uri): string { $components = parse_url($uri); $host = $components['host'] ?? ''; $path = $components['path'] ?? ''; $relative = PathHelper::join($host, $path); if (self::$rootPrefix !== '') { $relative = PathHelper::join(self::$rootPrefix, $relative); } return $relative; } private function fileExists(string $key): bool { try { return self::$filesystem->fileExists($key); } catch (FilesystemException) { return false; } } private function statKey(string $key): array|false { $this->ensureFilesystem(); try { if (self::$filesystem->fileExists($key)) { $size = 0; $mtime = time(); try { $size = self::$filesystem->fileSize($key); } catch (FilesystemException) { $size = 0; } try { $mtime = self::$filesystem->lastModified($key); } catch (FilesystemException) { $mtime = time(); } return $this->formatStat(0100644, $size, $mtime, 1); } if (self::$filesystem->directoryExists($key)) { $mtime = time(); try { $mtime = self::$filesystem->lastModified($key); } catch (FilesystemException) { $mtime = time(); } return $this->formatStat(0040755, 0, $mtime, 2); } } catch (FilesystemException) { return false; } return false; } /** * @return array{ * 0:int,1:int,2:int,3:int,4:int,5:int,6:int,7:int,8:int,9:int,10:int,11:int,12:int, * dev:int,ino:int,mode:int,nlink:int,uid:int,gid:int,rdev:int,size:int,atime:int,mtime:int,ctime:int,blksize:int,blocks:int * } */ private function formatStat(int $mode, int $size, int $timestamp, int $nlink = 1): array { return [ 0 => 0, 'dev' => 0, 1 => 0, 'ino' => 0, 2 => $mode, 'mode' => $mode, 3 => $nlink, 'nlink' => $nlink, 4 => 0, 'uid' => 0, 5 => 0, 'gid' => 0, 6 => 0, 'rdev' => 0, 7 => $size, 'size' => $size, 8 => $timestamp, 'atime' => $timestamp, 9 => $timestamp, 'mtime' => $timestamp, 10 => $timestamp, 'ctime' => $timestamp, 11 => -1, 'blksize' => -1, 12 => -1, 'blocks' => -1, ]; } private function ensureFilesystem(): void { if (! self::$filesystem) { throw new \RuntimeException('Flysystem filesystem has not been registered.'); } } }