├── phpstan.neon.dist ├── src ├── Flysystem │ ├── Exception │ │ ├── StatFailedException.php │ │ ├── IsDirectoryException.php │ │ ├── IsNotDirectoryException.php │ │ ├── RootDirectoryException.php │ │ ├── DirectoryExistsException.php │ │ ├── UnableToReadException.php │ │ ├── DirectoryNotEmptyException.php │ │ ├── DirectoryNotFoundException.php │ │ ├── FileNotFoundException.php │ │ ├── UnableToWriteException.php │ │ ├── CouldNotDeleteFileException.php │ │ ├── CouldNotRemoveDirectoryException.php │ │ ├── UnableToCreateDirectoryException.php │ │ ├── StreamWrapperException.php │ │ ├── UnableToChangePermissionsException.php │ │ └── InvalidStreamModeException.php │ ├── StreamCommand │ │ ├── StreamEofCommand.php │ │ ├── StreamTruncateCommand.php │ │ ├── StreamSeekCommand.php │ │ ├── StreamReadCommand.php │ │ ├── StreamCastCommand.php │ │ ├── StreamTellCommand.php │ │ ├── DirRewinddirCommand.php │ │ ├── DirReaddirCommand.php │ │ ├── StreamWriteCommand.php │ │ ├── ExceptionHandler.php │ │ ├── UrlStatCommand.php │ │ ├── UnlinkCommand.php │ │ ├── StreamSetOptionCommand.php │ │ ├── StreamLockCommand.php │ │ ├── MkdirCommand.php │ │ ├── DirOpendirCommand.php │ │ ├── RenameCommand.php │ │ ├── RmdirCommand.php │ │ ├── StreamMetadataCommand.php │ │ ├── StreamOpenCommand.php │ │ └── StreamStatCommand.php │ ├── Helper │ │ └── UserGuesser.php │ ├── FileData.php │ └── StreamWrapper.php └── FlysystemStreamWrapper.php ├── LICENSE.md ├── phpunit.10.xml ├── rector.php ├── composer.json ├── CHANGELOG.md └── README.md /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 9 3 | paths: 4 | - src 5 | - tests 6 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/StatFailedException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class StatFailedException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Stat failed'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/IsDirectoryException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class IsDirectoryException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Is a directory'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/IsNotDirectoryException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class IsNotDirectoryException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Not a directory'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/RootDirectoryException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class RootDirectoryException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Directory is root'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/DirectoryExistsException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class DirectoryExistsException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Directory exists'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/UnableToReadException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class UnableToReadException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Unable to read to file'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/DirectoryNotEmptyException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class DirectoryNotEmptyException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Directory not empty'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/DirectoryNotFoundException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class DirectoryNotFoundException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Failed to open dir'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/FileNotFoundException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class FileNotFoundException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'No such file or directory'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/UnableToWriteException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class UnableToWriteException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Unable to write to file'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/CouldNotDeleteFileException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class CouldNotDeleteFileException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Could not delete file'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/CouldNotRemoveDirectoryException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class CouldNotRemoveDirectoryException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Could not remove directory'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/UnableToCreateDirectoryException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | final class UnableToCreateDirectoryException extends StreamWrapperException 13 | { 14 | protected const ERROR_MESSAGE = 'Cannot create directory'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamEofCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class StreamEofCommand 15 | { 16 | public static function run(FileData $current): bool 17 | { 18 | if (!is_resource($current->handle)) { 19 | return false; 20 | } 21 | 22 | return feof($current->handle); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamTruncateCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class StreamTruncateCommand 15 | { 16 | public static function run(FileData $current, int $new_size): bool 17 | { 18 | if (!is_resource($current->handle) || $new_size < 0) { 19 | return false; 20 | } 21 | 22 | return ftruncate($current->handle, $new_size); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamSeekCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class StreamSeekCommand 15 | { 16 | public static function run(FileData $current, int $offset, int $whence = SEEK_SET): bool 17 | { 18 | if (!is_resource($current->handle)) { 19 | return false; 20 | } 21 | 22 | return 0 === fseek($current->handle, $offset, $whence); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamReadCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class StreamReadCommand 15 | { 16 | public static function run(FileData $current, int $count): string 17 | { 18 | if ($current->writeOnly || !is_resource($current->handle) || $count < 0) { 19 | return ''; 20 | } 21 | 22 | return (string) fread($current->handle, $count); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamCastCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class StreamCastCommand 15 | { 16 | /** 17 | * @noinspection PhpMissingReturnTypeInspection 18 | * @noinspection PhpUnusedParameterInspection 19 | * 20 | * @return resource|false 21 | */ 22 | public static function run(FileData $current, int $cast_as) 23 | { 24 | return $current->handle; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamTellCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class StreamTellCommand 15 | { 16 | public static function run(FileData $current): int 17 | { 18 | if (!is_resource($current->handle)) { 19 | return 0; 20 | } 21 | 22 | if ($current->alwaysAppend && $current->writeOnly) { 23 | return 0; 24 | } 25 | 26 | return (int) ftell($current->handle); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/DirRewinddirCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 14 | 15 | final class DirRewinddirCommand 16 | { 17 | use ExceptionHandler; 18 | 19 | public static function run(FileData $current): bool 20 | { 21 | try { 22 | DirOpendirCommand::getDir($current); 23 | } catch (FilesystemException $e) { 24 | return self::triggerError($e); 25 | } 26 | 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/DirReaddirCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class DirReaddirCommand 15 | { 16 | /** 17 | * @return string|false 18 | */ 19 | public static function run(FileData $current) 20 | { 21 | if (!$current->dirListing->valid()) { 22 | return false; 23 | } 24 | 25 | $item = $current->dirListing->current(); 26 | 27 | $current->dirListing->next(); 28 | 29 | return basename($item->path()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/StreamWrapperException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use RuntimeException; 14 | use Throwable; 15 | 16 | class StreamWrapperException extends RuntimeException implements FilesystemException 17 | { 18 | protected const ERROR_MESSAGE = 'Error message not defined'; 19 | 20 | public static function atLocation( 21 | string $command, 22 | string $location, 23 | Throwable $previous = null 24 | ): StreamWrapperException { 25 | return new self("$command($location): ".static::ERROR_MESSAGE, 0, $previous); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/UnableToChangePermissionsException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use RuntimeException; 14 | use Throwable; 15 | 16 | final class UnableToChangePermissionsException extends RuntimeException implements FilesystemException 17 | { 18 | public static function atLocation( 19 | string $command, 20 | string $location, 21 | string $permission, 22 | Throwable $previous = null 23 | ): UnableToChangePermissionsException { 24 | return new self("$command($location,$permission): Unable to change permissions", 0, $previous); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/InvalidStreamModeException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Exception; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use RuntimeException; 14 | use Throwable; 15 | 16 | final class InvalidStreamModeException extends RuntimeException implements FilesystemException 17 | { 18 | public static function atLocation( 19 | string $command, 20 | string $location, 21 | string $mode, 22 | Throwable $previous = null 23 | ): InvalidStreamModeException { 24 | return new self( 25 | "$command($location): Failed to open stream: '$mode' is not a valid mode", 26 | 0, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamWriteCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class StreamWriteCommand 15 | { 16 | public static function run(FileData $current, string $data): int 17 | { 18 | if (!is_resource($current->handle)) { 19 | return 0; 20 | } 21 | 22 | if ($current->alwaysAppend) { 23 | fseek($current->handle, 0, SEEK_END); 24 | } 25 | 26 | $size = (int) fwrite($current->handle, $data); 27 | $current->bytesWritten += $size; 28 | 29 | if ($current->alwaysAppend) { 30 | fseek($current->handle, 0, SEEK_SET); 31 | } 32 | 33 | return $size; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/ExceptionHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use Throwable; 14 | 15 | trait ExceptionHandler 16 | { 17 | public static function triggerError(FilesystemException $e): bool 18 | { 19 | trigger_error(self::collectErrorMessage($e), E_USER_WARNING); 20 | 21 | return false; 22 | } 23 | 24 | protected static function collectErrorMessage(Throwable $e): string 25 | { 26 | $message = $e->getMessage(); 27 | $previous = $e->getPrevious(); 28 | if (!$previous instanceof Throwable) { 29 | return $message; 30 | } 31 | 32 | return $message.' : '.self::collectErrorMessage($previous); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2021 m2m server software gmbh 4 | Portions Copyright (c) 2015 Chris Leppanen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /phpunit.10.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | tests 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/UrlStatCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\StatFailedException; 14 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 15 | 16 | final class UrlStatCommand 17 | { 18 | use ExceptionHandler; 19 | 20 | public const URL_STAT_COMMAND = 'url_stat'; 21 | 22 | /** @return array|false */ 23 | public static function run(FileData $current, string $path, int $flags) 24 | { 25 | $current->setPath($path); 26 | 27 | try { 28 | return StreamStatCommand::getStat($current); 29 | } catch (FilesystemException $e) { 30 | if (($flags & STREAM_URL_STAT_QUIET) !== 0) { 31 | return false; 32 | } 33 | 34 | self::triggerError(StatFailedException::atLocation(self::URL_STAT_COMMAND, $path, $e)); 35 | 36 | return false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/UnlinkCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\CouldNotDeleteFileException; 14 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\FileNotFoundException; 15 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 16 | 17 | final class UnlinkCommand 18 | { 19 | use ExceptionHandler; 20 | 21 | public const UNLINK_COMMAND = 'unlink'; 22 | 23 | public static function run(FileData $current, string $path): bool 24 | { 25 | $current->setPath($path); 26 | 27 | if (!file_exists($current->path)) { 28 | return self::triggerError(FileNotFoundException::atLocation(self::UNLINK_COMMAND, $current->path)); 29 | } 30 | 31 | try { 32 | $current->filesystem->delete($current->file); 33 | 34 | return true; 35 | } catch (FilesystemException $e) { 36 | return self::triggerError( 37 | CouldNotDeleteFileException::atLocation(self::UNLINK_COMMAND, $current->path, $e) 38 | ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamSetOptionCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | 14 | final class StreamSetOptionCommand 15 | { 16 | public static function run(FileData $current, int $option, int $arg1, ?int $arg2): bool 17 | { 18 | if (!is_resource($current->handle)) { 19 | return false; 20 | } 21 | 22 | switch ($option) { 23 | case STREAM_OPTION_BLOCKING: 24 | return stream_set_blocking($current->handle, 1 === $arg1); 25 | 26 | case STREAM_OPTION_READ_BUFFER: 27 | return 0 === stream_set_read_buffer( 28 | $current->handle, 29 | STREAM_BUFFER_NONE === $arg1 ? 0 : (int) $arg2 30 | ); 31 | 32 | case STREAM_OPTION_WRITE_BUFFER: 33 | $current->writeBufferSize = STREAM_BUFFER_NONE === $arg1 ? 0 : (int) $arg2; 34 | 35 | return true; 36 | 37 | case STREAM_OPTION_READ_TIMEOUT: 38 | return stream_set_timeout($current->handle, $arg1, (int) $arg2); 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Flysystem/Helper/UserGuesser.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\Helper; 11 | 12 | class UserGuesser 13 | { 14 | /** @var ?int */ 15 | private static $uid = null; 16 | 17 | /** @var ?int */ 18 | private static $gid = null; 19 | 20 | private static function useFallback(): void 21 | { 22 | self::$uid = (int) getmyuid(); 23 | self::$gid = (int) getmygid(); 24 | } 25 | 26 | private static function guess(): void 27 | { 28 | if (null !== self::$uid) { 29 | return; 30 | } 31 | 32 | $file = tempnam(sys_get_temp_dir(), 'UserGuesser'); 33 | if (!$file) { 34 | self::useFallback(); 35 | 36 | return; 37 | } 38 | 39 | file_put_contents($file, 'guessing'); 40 | 41 | $stats = stat($file); 42 | if (!$stats) { 43 | self::useFallback(); 44 | 45 | return; 46 | } 47 | 48 | self::$uid = $stats['uid']; 49 | self::$gid = $stats['gid']; 50 | 51 | unlink($file); 52 | } 53 | 54 | public static function getUID(): int 55 | { 56 | self::guess(); 57 | 58 | return (int) self::$uid; 59 | } 60 | 61 | public static function getGID(): int 62 | { 63 | return (int) self::$gid; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | use Rector\Core\Configuration\Option; 13 | use Rector\Core\ValueObject\PhpVersion; 14 | use Rector\Set\ValueObject\SetList; 15 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 16 | 17 | return static function (ContainerConfigurator $containerConfigurator): void { 18 | // get parameters 19 | $parameters = $containerConfigurator->parameters(); 20 | $parameters->set(Option::PATHS, [ 21 | __DIR__ . '/src' 22 | ]); 23 | 24 | $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_72); 25 | 26 | $containerConfigurator->import(SetList::PHP_81); 27 | $containerConfigurator->import(SetList::CODE_QUALITY); 28 | $containerConfigurator->import(SetList::PRIVATIZATION); 29 | // $containerConfigurator->import(SetList::TYPE_DECLARATION); 30 | // $containerConfigurator->import(SetList::TYPE_DECLARATION_STRICT); 31 | // $containerConfigurator->import(SetList::NAMING); 32 | // $containerConfigurator->import(SetList::EARLY_RETURN); 33 | // $containerConfigurator->import(SetList::CODING_STYLE); 34 | // $containerConfigurator->import(SetList::DEAD_CODE); 35 | 36 | // get services (needed for register a single rule) 37 | // $services = $containerConfigurator->services(); 38 | 39 | // register a single rule 40 | // $services->set(TypedPropertyRector::class); 41 | }; 42 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamLockCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 13 | use M2MTech\FlysystemStreamWrapper\FlysystemStreamWrapper; 14 | use Symfony\Component\Lock\Key; 15 | use Symfony\Component\Lock\Lock; 16 | use Symfony\Component\Lock\Store\StoreFactory; 17 | 18 | final class StreamLockCommand 19 | { 20 | public static function run(FileData $current, int $operation): bool 21 | { 22 | if (null === $current->lockKey) { 23 | $current->lockKey = new Key($current->path); 24 | } 25 | 26 | $store = StoreFactory::createStore((string) $current->config[FlysystemStreamWrapper::LOCK_STORE]); 27 | $lock = new Lock( 28 | $current->lockKey, 29 | $store, 30 | (float) $current->config[FlysystemStreamWrapper::LOCK_TTL], 31 | false 32 | ); 33 | 34 | switch ($operation) { 35 | case LOCK_SH: 36 | return $lock->acquireRead(true); 37 | 38 | case LOCK_EX: 39 | return $lock->acquire(true); 40 | 41 | case LOCK_UN: 42 | $lock->release(); 43 | 44 | return true; 45 | 46 | case LOCK_SH | LOCK_NB: 47 | return $lock->acquireRead(); 48 | 49 | case LOCK_EX | LOCK_NB: 50 | return $lock->acquire(); 51 | } 52 | 53 | return false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/MkdirCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\Config; 13 | use League\Flysystem\FilesystemException; 14 | use League\Flysystem\UnixVisibility\PortableVisibilityConverter; 15 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\DirectoryExistsException; 16 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\UnableToCreateDirectoryException; 17 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 18 | 19 | final class MkdirCommand 20 | { 21 | use ExceptionHandler; 22 | 23 | public const MKDIR_COMMAND = 'mkdir'; 24 | 25 | /** @noinspection PhpUnusedParameterInspection */ 26 | public static function run(FileData $current, string $path, int $mode, int $options): bool 27 | { 28 | if (file_exists($path)) { 29 | return self::triggerError(DirectoryExistsException::atLocation(self::MKDIR_COMMAND, $path)); 30 | } 31 | 32 | $current->setPath($path); 33 | 34 | try { 35 | $visibility = new PortableVisibilityConverter(); 36 | $config = [ 37 | Config::OPTION_VISIBILITY => $visibility->inverseForDirectory($mode), 38 | ]; 39 | $current->filesystem->createDirectory($current->file, $config); 40 | 41 | return true; 42 | } catch (FilesystemException $e) { 43 | return self::triggerError( 44 | UnableToCreateDirectoryException::atLocation(self::MKDIR_COMMAND, $path, $e) 45 | ); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/DirOpendirCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use Iterator; 13 | use IteratorIterator; 14 | use League\Flysystem\FilesystemException; 15 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\DirectoryNotFoundException; 16 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 17 | 18 | final class DirOpendirCommand 19 | { 20 | use ExceptionHandler; 21 | 22 | public const OPENDIR_COMMAND = 'dir_opendir'; 23 | 24 | /** @noinspection PhpUnusedParameterInspection */ 25 | public static function run(FileData $current, string $path, int $options): bool 26 | { 27 | $current->setPath($path); 28 | try { 29 | self::getDir($current); 30 | } catch (FilesystemException $e) { 31 | return self::triggerError( 32 | DirectoryNotFoundException::atLocation(self::OPENDIR_COMMAND, $path, $e) 33 | ); 34 | } 35 | 36 | $valid = @is_dir($path); 37 | if (!$valid) { 38 | return self::triggerError( 39 | DirectoryNotFoundException::atLocation(self::OPENDIR_COMMAND, $path) 40 | ); 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /** @throws FilesystemException */ 47 | public static function getDir(FileData $current): void 48 | { 49 | $listing = $current->filesystem->listContents($current->file)->getIterator(); 50 | $current->dirListing = $listing instanceof Iterator ? $listing : new IteratorIterator($listing); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "m2mtech/flysystem-stream-wrapper", 3 | "type": "library", 4 | "description": "A stream wrapper for Flysystem V2 & V3.", 5 | "keywords": [ 6 | "flysystem", 7 | "streamwrapper", 8 | "stream-wrapper" 9 | ], 10 | "homepage": "https://github.com/m2mtech/flysystem-stream-wrapper", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Martin Mandl", 15 | "email": "tech@m2m.at", 16 | "homepage": "https://cms.m2m.at", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.2 || ^8.0", 22 | "league/flysystem": "^2.3|^3.0", 23 | "symfony/lock": "^5.3|^6.0|^7.0" 24 | }, 25 | "replace": { 26 | "twistor/flysystem-stream-wrapper": "v1.0.9" 27 | }, 28 | "require-dev": { 29 | "ext-intl": "*", 30 | "ext-pcntl": "*", 31 | "ext-shmop": "*", 32 | "ext-sysvmsg": "*", 33 | "amphp/amp": "^2.6", 34 | "amphp/parallel": "^v1.4", 35 | "amphp/parallel-functions": "^1.0", 36 | "amphp/sync": "^1.4", 37 | "fakerphp/faker": "^1.19", 38 | "opis/closure": "^3.6", 39 | "phpstan/phpstan": "^1.10", 40 | "phpunit/php-invoker": "^2.0|^3.1|^4.0", 41 | "phpunit/phpunit": "^8.5|^9.6|^10.0", 42 | "symplify/easy-coding-standard": "^12.0" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "M2MTech\\FlysystemStreamWrapper\\": "src" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "M2MTech\\FlysystemStreamWrapper\\Tests\\": "tests" 52 | } 53 | }, 54 | "scripts": { 55 | "test": "vendor/bin/phpunit", 56 | "test10": "vendor/bin/phpunit -c phpunit.10.xml", 57 | "check-cs": "vendor/bin/ecs check", 58 | "fix-cs": "vendor/bin/ecs check --fix", 59 | "phpstan": "vendor/bin/phpstan analyse --memory-limit 1G" 60 | }, 61 | "config": { 62 | "sort-packages": true 63 | }, 64 | "minimum-stability": "dev", 65 | "prefer-stable": true 66 | } 67 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/RenameCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\DirectoryNotEmptyException; 14 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\FileNotFoundException; 15 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\IsDirectoryException; 16 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\IsNotDirectoryException; 17 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 18 | 19 | final class RenameCommand 20 | { 21 | use ExceptionHandler; 22 | 23 | public const RENAME_COMMAND = 'rename'; 24 | 25 | public static function run(FileData $current, string $path_from, string $path_to): bool 26 | { 27 | $current->setPath($path_from); 28 | 29 | $errorLocation = $path_from.','.$path_to; 30 | if (!file_exists($path_from)) { 31 | return self::triggerError(FileNotFoundException::atLocation(self::RENAME_COMMAND, $errorLocation)); 32 | } 33 | 34 | if (file_exists($path_to)) { 35 | if (is_file($path_from) && is_dir($path_to)) { 36 | return self::triggerError( 37 | IsDirectoryException::atLocation(self::RENAME_COMMAND, $errorLocation) 38 | ); 39 | } 40 | if (is_dir($path_from) && is_file($path_to)) { 41 | return self::triggerError( 42 | IsNotDirectoryException::atLocation(self::RENAME_COMMAND, $errorLocation) 43 | ); 44 | } 45 | } 46 | 47 | try { 48 | $current->filesystem->move($current->file, FileData::getFile($path_to)); 49 | 50 | return true; 51 | } catch (FilesystemException $e) { 52 | return self::triggerError( 53 | DirectoryNotEmptyException::atLocation(self::RENAME_COMMAND, $errorLocation, $e) 54 | ); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Flysystem/FileData.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem; 11 | 12 | use Iterator; 13 | use League\Flysystem\FilesystemOperator; 14 | use League\Flysystem\StorageAttributes; 15 | use M2MTech\FlysystemStreamWrapper\FlysystemStreamWrapper; 16 | use Symfony\Component\Lock\Key; 17 | 18 | final class FileData 19 | { 20 | /** @var string */ 21 | public $path; 22 | 23 | /** @var string */ 24 | public $protocol; 25 | 26 | /** @var string */ 27 | public $file; 28 | 29 | /** @var FilesystemOperator */ 30 | public $filesystem; 31 | 32 | /** @var array */ 33 | public $config = []; 34 | 35 | /** @var resource|false */ 36 | public $handle = false; 37 | 38 | /** @var bool */ 39 | public $writeOnly = false; 40 | 41 | /** @var bool */ 42 | public $alwaysAppend = false; 43 | 44 | /** @var bool */ 45 | public $workOnLocalCopy = false; 46 | 47 | /** @var int */ 48 | public $writeBufferSize = 0; 49 | 50 | /** @var int */ 51 | public $bytesWritten = 0; 52 | 53 | /** @var Key */ 54 | public $lockKey; 55 | 56 | /** @var Iterator */ 57 | public $dirListing; 58 | 59 | public function setPath(string $path): void 60 | { 61 | $this->path = $path; 62 | $this->protocol = substr($path, 0, (int) strpos($path, '://')); 63 | $this->file = self::getFile($path); 64 | $this->filesystem = FlysystemStreamWrapper::$filesystems[$this->protocol]; 65 | $this->config = FlysystemStreamWrapper::$config[$this->protocol]; 66 | } 67 | 68 | public static function getFile(string $path): string 69 | { 70 | return (string) substr($path, strpos($path, '://') + 3); 71 | } 72 | 73 | public function ignoreVisibilityErrors(): bool 74 | { 75 | return (bool) $this->config[FlysystemStreamWrapper::IGNORE_VISIBILITY_ERRORS]; 76 | } 77 | 78 | public function emulateDirectoryLastModified(): bool 79 | { 80 | return (bool) $this->config[FlysystemStreamWrapper::EMULATE_DIRECTORY_LAST_MODIFIED]; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/RmdirCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use League\Flysystem\StorageAttributes; 14 | use League\Flysystem\WhitespacePathNormalizer; 15 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\CouldNotRemoveDirectoryException; 16 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\DirectoryNotEmptyException; 17 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\RootDirectoryException; 18 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 19 | 20 | final class RmdirCommand 21 | { 22 | use ExceptionHandler; 23 | 24 | public const RMDIR_COMMAND = 'rmdir'; 25 | 26 | public static function run(FileData $current, string $path, int $options): bool 27 | { 28 | $current->setPath($path); 29 | 30 | $n = new WhitespacePathNormalizer(); 31 | $n->normalizePath($current->file); 32 | if ('' === $n->normalizePath($current->file)) { 33 | return self::triggerError( 34 | RootDirectoryException::atLocation(self::RMDIR_COMMAND, $current->path) 35 | ); 36 | } 37 | 38 | if (($options & STREAM_MKDIR_RECURSIVE) !== 0) { 39 | return self::rmdir($current); 40 | } 41 | 42 | try { 43 | $listing = $current->filesystem->listContents($current->file); 44 | } catch (FilesystemException $e) { 45 | return self::triggerError( 46 | DirectoryNotEmptyException::atLocation(self::RMDIR_COMMAND, $current->path) 47 | ); 48 | } 49 | 50 | foreach ($listing as $ignored) { 51 | if (!$ignored instanceof StorageAttributes) { 52 | continue; 53 | } 54 | 55 | return self::triggerError( 56 | DirectoryNotEmptyException::atLocation(self::RMDIR_COMMAND, $current->path) 57 | ); 58 | } 59 | 60 | return self::rmdir($current); 61 | } 62 | 63 | private static function rmdir(FileData $current): bool 64 | { 65 | try { 66 | $current->filesystem->deleteDirectory($current->file); 67 | 68 | return true; 69 | } catch (FilesystemException $e) { 70 | return self::triggerError( 71 | CouldNotRemoveDirectoryException::atLocation(self::RMDIR_COMMAND, $current->path, $e) 72 | ); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamMetadataCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use League\Flysystem\UnixVisibility\PortableVisibilityConverter; 14 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\UnableToChangePermissionsException; 15 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\UnableToWriteException; 16 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 17 | 18 | final class StreamMetadataCommand 19 | { 20 | use ExceptionHandler; 21 | 22 | public const METADATA_COMMAND = 'stream_metadata'; 23 | 24 | /** @param mixed $value */ 25 | public static function run(FileData $current, string $path, int $option, $value): bool 26 | { 27 | $current->setPath($path); 28 | $filesystem = $current->filesystem; 29 | $file = $current->file; 30 | 31 | switch ($option) { 32 | case STREAM_META_ACCESS: 33 | if (!is_int($value)) { 34 | /* @phpstan-ignore-next-line */ 35 | $value = (int) $value; 36 | } 37 | 38 | $converter = new PortableVisibilityConverter(); 39 | $visibility = is_dir($path) ? $converter->inverseForDirectory($value) : $converter->inverseForFile($value); 40 | 41 | try { 42 | $filesystem->setVisibility($file, $visibility); 43 | } catch (FilesystemException $e) { 44 | if (!$current->ignoreVisibilityErrors()) { 45 | return self::triggerError(UnableToChangePermissionsException::atLocation( 46 | self::METADATA_COMMAND, 47 | $current->path, 48 | decoct($value), 49 | $e 50 | )); 51 | } 52 | } 53 | 54 | return true; 55 | 56 | case STREAM_META_TOUCH: 57 | try { 58 | if (!$filesystem->fileExists($file)) { 59 | $filesystem->write($file, ''); 60 | } 61 | } catch (FilesystemException $e) { 62 | return self::triggerError(UnableToWriteException::atLocation( 63 | self::METADATA_COMMAND, 64 | $current->path, 65 | $e 66 | )); 67 | } 68 | 69 | return true; 70 | 71 | default: 72 | return false; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.0] - 2023-12-18 4 | - compatibility with symfony 7.x & php 8.3 5 | - allow ecs 12.x 6 | - added `EMULATE_DIRECTORY_LAST_MODIFIED`, thanks to @das-peter 7 | 8 | ## [1.3.3] - 2023-03-27 9 | - fixed tests for phpunit v10 10 | 11 | ## [1.3.2] - 2023-03-21 12 | - allow `bool` in `FlysystemStreamWrapper::register` configuration 13 | - fixed phpunit deprecations 14 | - updated (c) dates 15 | 16 | ## [1.3.1] - 2022-08-12 17 | - intercept TypeError when adapter returns null for visibility 18 | 19 | ## [1.3.0] - 2022-07-21 20 | - adjusted uid & gid retrieval 21 | - added manual uid & gid setup 22 | - allow to access parameters of [`PortableVisibilityConverter`](https://flysystem.thephpleague.com/docs/usage/unix-visibility/) 23 | - fixed typo in const `IGNORE_VISIBILITY_ERRORS` 24 | 25 | ## [1.2.1] - 2022-06-10 26 | - added intl for dev to fix ecs dependency 27 | - fix for resources closed by flysystem prematurely 28 | 29 | ## [1.2.0] - 2022-06-04 30 | - set current user uid & gid in stats 31 | 32 | ## [1.1.0] - 2022-05-05 33 | - allow for adaptors closing the file handle themselves 34 | - fixed broken directory detection caused by changed MimeType detection of [Flysystem 3.0.16](https://github.com/thephpleague/flysystem/compare/3.0.15...3.0.16) 35 | - allow to ignore visibility errors 36 | 37 | ## [1.0.3] - 2022-01-21 38 | - allow flysystem v3 39 | 40 | ## [1.0.2] - 2022-01-10 41 | - replace section in [composer.json](composer.json) 42 | 43 | ## [1.0.1] - 2021-12-29 44 | - compatibility with symfony 6.x 45 | - skipped unit tests of locking for php8.1 till opis/closure is available in version 4.x (only used for testing) 46 | 47 | ## [1.0.0] - 2021-11-01 48 | - Initial release 49 | 50 | 55 | 56 | [Unreleased]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.3.3...HEAD 57 | [1.4.0]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.3.3...v1.4.0 58 | [1.3.3]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.3.2...v1.3.3 59 | [1.3.2]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.3.1...v1.3.2 60 | [1.3.1]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.3.0...v1.3.1 61 | [1.3.0]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.2.1...v1.3.0 62 | [1.2.1]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.2.0...v1.2.1 63 | [1.2.0]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.1.0...v1.2.0 64 | [1.1.0]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.0.3...v1.1.0 65 | [1.0.3]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.0.2...v1.0.3 66 | [1.0.2]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.0.1...v1.0.2 67 | [1.0.1]: https://github.com/m2mtech/flysystem-stream-wrapper/compare/v1.0.0...v1.0.1 68 | [1.0.0]: https://github.com/m2mtech/flysystem-stream-wrapper/releases/tag/v1.0.0 69 | 72 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamOpenCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\FileNotFoundException; 14 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\InvalidStreamModeException; 15 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\UnableToReadException; 16 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\UnableToWriteException; 17 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 18 | 19 | final class StreamOpenCommand 20 | { 21 | use ExceptionHandler; 22 | 23 | public const OPEN_COMMAND = 'stream_open'; 24 | 25 | public static function run( 26 | FileData $current, 27 | string $path, 28 | string $mode, 29 | int $options, 30 | ?string &$openedPath 31 | ): bool { 32 | $current->setPath($path); 33 | $filesystem = $current->filesystem; 34 | $file = $current->file; 35 | 36 | if (!preg_match('/^[rwacx](\+b?|b\+?)?$/', $mode)) { 37 | return self::triggerError(InvalidStreamModeException::atLocation( 38 | self::OPEN_COMMAND, 39 | $current->path, 40 | $mode 41 | )); 42 | } 43 | 44 | $current->writeOnly = !strpos($mode, '+'); 45 | try { 46 | if ('r' === $mode[0] && $current->writeOnly) { 47 | $current->handle = $filesystem->readStream($file); 48 | $current->workOnLocalCopy = false; 49 | $current->writeOnly = false; 50 | } else { 51 | $current->handle = fopen('php://temp', 'w+b'); 52 | $current->workOnLocalCopy = true; 53 | 54 | if ('w' !== $mode[0] && $filesystem->fileExists($file)) { 55 | if ('x' === $mode[0]) { 56 | throw UnableToWriteException::atLocation(self::OPEN_COMMAND, $current->path); 57 | } 58 | 59 | $result = false; 60 | if (is_resource($current->handle)) { 61 | $result = stream_copy_to_stream($filesystem->readStream($file), $current->handle); 62 | } 63 | if (!$result) { 64 | throw UnableToWriteException::atLocation(self::OPEN_COMMAND, $current->path); 65 | } 66 | } 67 | } 68 | 69 | $current->alwaysAppend = 'a' === $mode[0]; 70 | if (is_resource($current->handle) && !$current->alwaysAppend) { 71 | @rewind($current->handle); 72 | } 73 | } catch (FilesystemException $e) { 74 | if (($options & STREAM_REPORT_ERRORS) !== 0) { 75 | return self::triggerError(UnableToReadException::atLocation( 76 | self::OPEN_COMMAND, 77 | $current->path, 78 | $e 79 | )); 80 | } 81 | 82 | return false; 83 | } 84 | 85 | if ($current->handle && $options & STREAM_USE_PATH) { 86 | $openedPath = $path; 87 | } 88 | 89 | if (is_resource($current->handle)) { 90 | return true; 91 | } 92 | 93 | if (($options & STREAM_REPORT_ERRORS) !== 0) { 94 | return self::triggerError(FileNotFoundException::atLocation(self::OPEN_COMMAND, $current->path)); 95 | } 96 | 97 | return false; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/FlysystemStreamWrapper.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper; 11 | 12 | use League\Flysystem\FilesystemOperator; 13 | use League\Flysystem\Visibility; 14 | use M2MTech\FlysystemStreamWrapper\Flysystem\Helper\UserGuesser; 15 | use M2MTech\FlysystemStreamWrapper\Flysystem\StreamWrapper; 16 | 17 | final class FlysystemStreamWrapper 18 | { 19 | public const LOCK_STORE = 'lock_store'; 20 | public const LOCK_TTL = 'lock_ttl'; 21 | 22 | /** @deprecated 2.0.0 use FlysystemStreamWrapper::IGNORE_VISIBILITY_ERRORS instead */ 23 | public const IGNORE_VISIBILITY_ERROS = 'ignore_visibility_errors'; 24 | 25 | public const IGNORE_VISIBILITY_ERRORS = 'ignore_visibility_errors'; 26 | 27 | public const EMULATE_DIRECTORY_LAST_MODIFIED = 'emulate_directory_last_modified'; 28 | 29 | public const UID = 'uid'; 30 | public const GID = 'gid'; 31 | 32 | public const VISIBILITY_FILE_PUBLIC = 'visibility_file_public'; 33 | public const VISIBILITY_FILE_PRIVATE = 'visibility_file_private'; 34 | public const VISIBILITY_DIRECTORY_PUBLIC = 'visibility_directory_public'; 35 | public const VISIBILITY_DIRECTORY_PRIVATE = 'visibility_directory_private'; 36 | public const VISIBILITY_DEFAULT_FOR_DIRECTORIES = 'visibility_default_for_directories'; 37 | 38 | public const DEFAULT_CONFIGURATION = [ 39 | self::LOCK_STORE => 'flock:///tmp', 40 | self::LOCK_TTL => 300, 41 | 42 | self::IGNORE_VISIBILITY_ERRORS => false, 43 | self::EMULATE_DIRECTORY_LAST_MODIFIED => false, 44 | 45 | self::UID => null, 46 | self::GID => null, 47 | 48 | self::VISIBILITY_FILE_PUBLIC => 0644, 49 | self::VISIBILITY_FILE_PRIVATE => 0600, 50 | self::VISIBILITY_DIRECTORY_PUBLIC => 0755, 51 | self::VISIBILITY_DIRECTORY_PRIVATE => 0700, 52 | self::VISIBILITY_DEFAULT_FOR_DIRECTORIES => Visibility::PRIVATE, 53 | ]; 54 | 55 | /** @var array */ 56 | public static $filesystems = []; 57 | 58 | /** @var array > */ 59 | public static $config = []; 60 | 61 | /** @param array $configuration */ 62 | public static function register( 63 | string $protocol, 64 | FilesystemOperator $filesystem, 65 | array $configuration = [], 66 | int $flags = 0 67 | ): bool { 68 | if (self::streamWrapperExists($protocol)) { 69 | return false; 70 | } 71 | 72 | self::$config[$protocol] = array_merge(self::DEFAULT_CONFIGURATION, $configuration); 73 | self::$filesystems[$protocol] = $filesystem; 74 | 75 | if (null === self::$config[$protocol][self::UID]) { 76 | self::$config[$protocol][self::UID] = UserGuesser::getUID(); 77 | } 78 | 79 | if (null === self::$config[$protocol][self::GID]) { 80 | self::$config[$protocol][self::GID] = UserGuesser::getGID(); 81 | } 82 | 83 | return stream_wrapper_register($protocol, StreamWrapper::class, $flags); 84 | } 85 | 86 | public static function unregister(string $protocol): bool 87 | { 88 | if (!self::streamWrapperExists($protocol)) { 89 | return false; 90 | } 91 | 92 | unset(self::$config[$protocol], self::$filesystems[$protocol]); 93 | 94 | return stream_wrapper_unregister($protocol); 95 | } 96 | 97 | public static function unregisterAll(): void 98 | { 99 | foreach (self::getRegisteredProtocols() as $protocol) { 100 | self::unregister($protocol); 101 | } 102 | } 103 | 104 | /** @return array */ 105 | public static function getRegisteredProtocols(): array 106 | { 107 | return array_keys(self::$filesystems); 108 | } 109 | 110 | public static function streamWrapperExists(string $protocol): bool 111 | { 112 | return in_array($protocol, stream_get_wrappers(), true); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Flysystem/StreamWrapper.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem; 11 | 12 | use League\Flysystem\FilesystemException; 13 | use M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand\ExceptionHandler; 14 | use M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand\StreamWriteCommand; 15 | 16 | /** 17 | * @method url_stat(string $path, int $int) 18 | */ 19 | final class StreamWrapper 20 | { 21 | use ExceptionHandler; 22 | 23 | /** @var FileData */ 24 | private $current; 25 | 26 | public function __construct(?FileData $current = null) 27 | { 28 | $this->current = $current ?? new FileData(); 29 | } 30 | 31 | /** 32 | * @param array $args 33 | * 34 | * @return array|string|bool 35 | */ 36 | public function __call(string $method, array $args) 37 | { 38 | $class = __NAMESPACE__.'\\StreamCommand\\'.str_replace('_', '', ucwords($method, '_')).'Command'; 39 | if (class_exists($class)) { 40 | return $class::run($this->current, ...$args); 41 | } 42 | 43 | return false; 44 | } 45 | 46 | /** @var resource */ 47 | public $context; 48 | 49 | public function dir_closedir(): bool 50 | { 51 | unset($this->current->dirListing); 52 | 53 | return true; 54 | } 55 | 56 | public function stream_close(): void 57 | { 58 | if (!is_resource($this->current->handle)) { 59 | return; 60 | } 61 | 62 | if ($this->current->workOnLocalCopy) { 63 | fflush($this->current->handle); 64 | rewind($this->current->handle); 65 | 66 | try { 67 | $this->current->filesystem->writeStream($this->current->file, $this->current->handle); 68 | } catch (FilesystemException $e) { 69 | trigger_error( 70 | 'stream_close('.$this->current->path.') Unable to sync file : '.self::collectErrorMessage($e), 71 | E_USER_WARNING 72 | ); 73 | } 74 | } 75 | 76 | fclose($this->current->handle); 77 | } 78 | 79 | public function stream_flush(): bool 80 | { 81 | if (!is_resource($this->current->handle)) { 82 | trigger_error( 83 | 'stream_flush(): Supplied resource is not a valid stream resource', 84 | E_USER_WARNING 85 | ); 86 | 87 | return false; 88 | } 89 | 90 | $success = fflush($this->current->handle); 91 | 92 | if ($this->current->workOnLocalCopy) { 93 | fflush($this->current->handle); 94 | $currentPosition = ftell($this->current->handle); 95 | rewind($this->current->handle); 96 | 97 | try { 98 | $this->current->filesystem->writeStream($this->current->file, $this->current->handle); 99 | } catch (FilesystemException $e) { 100 | trigger_error( 101 | 'stream_flush('.$this->current->path.') Unable to sync file : '.self::collectErrorMessage($e), 102 | E_USER_WARNING 103 | ); 104 | $success = false; 105 | } 106 | 107 | if (false !== $currentPosition) { 108 | if (is_resource($this->current->handle)) { 109 | fseek($this->current->handle, $currentPosition); 110 | } 111 | } 112 | } 113 | 114 | $this->current->bytesWritten = 0; 115 | 116 | return $success; 117 | } 118 | 119 | /** @return array|false */ 120 | public function stream_stat() 121 | { 122 | return $this->url_stat($this->current->path, 0); 123 | } 124 | 125 | public function stream_write(string $data): int 126 | { 127 | $size = StreamWriteCommand::run($this->current, $data); 128 | 129 | if ($this->current->writeBufferSize && $this->current->bytesWritten >= $this->current->writeBufferSize) { 130 | $this->stream_flush(); 131 | } 132 | 133 | return $size; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flysystem Stream Wrapper 2 | 3 | [![Author](https://img.shields.io/badge/author-@m2mtech-blue.svg?style=flat-square)](http://www.m2m.at) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | 6 | --- 7 | 8 | This package provides a stream wrapper for Flysystem V2 & V3. 9 | 10 | ## Flysystem V1 11 | 12 | If you're looking for Flysystem 1.x support, check out the [twistor/flysystem-stream-wrapper](https://github.com/twistor/flysystem-stream-wrapper). 13 | 14 | ## Installation 15 | 16 | ```bash 17 | composer require m2mtech/flysystem-stream-wrapper 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```php 23 | use League\Flysystem\Filesystem; 24 | use League\Flysystem\Local\LocalFilesystemAdapter; 25 | use M2MTech\FlysystemStreamWrapper\FlysystemStreamWrapper; 26 | 27 | $filesystem = new Filesystem(new LocalFilesystemAdapter('/some/path')); 28 | FlysystemStreamWrapper::register('fly', $filesystem); 29 | 30 | file_put_contents('fly://filename.txt', $content); 31 | mkdir('fly://happy_thoughts'); 32 | 33 | FlysystemStreamWrapper::unregister('fly'); 34 | ``` 35 | 36 | The stream wrapper implements [`symfony/lock`](https://symfony.com/doc/current/components/lock.html) due to Flysystem V2 not supporting locking. By default, file locking using `/tmp` is used, but you can adjust this through configuration: 37 | 38 | ```php 39 | FlysystemStreamWrapper::register('fly', $filesystem, [ 40 | FlysystemStreamWrapper::LOCK_STORE => 'flock:///tmp', 41 | FlysystemStreamWrapper::LOCK_TTL => 300, 42 | ]); 43 | ``` 44 | 45 | ### Handling Visibility Issues 46 | 47 | Some adaptors might throw exceptions when dealing with visibility. If you encounter such issues, configure the stream wrapper to bypass them: 48 | 49 | ```php 50 | FlysystemStreamWrapper::register('fly', $filesystem, [ 51 | FlysystemStreamWrapper::IGNORE_VISIBILITY_ERRORS => true, 52 | ]); 53 | ``` 54 | 55 | ### Addressing Directory Issues (`file_exists` / `is_dir`) 56 | 57 | Some adaptors might not return dates for the last modified attribute for directories. In such cases, you can enable emulation to achieve the desired behavior: 58 | 59 | ```php 60 | FlysystemStreamWrapper::register('fly', $filesystem, [ 61 | FlysystemStreamWrapper::EMULATE_DIRECTORY_LAST_MODIFIED => true, 62 | ]); 63 | ``` 64 | 65 | ### Dealing with `is_readable` / `is_writable` 66 | 67 | Some filesystem functions depend on the `uid` and `gid` of the user executing PHP. Since a reliable cross-platform method to derive these values isn't available, the wrapper attempts to estimate them. If this fails, set them manually: 68 | 69 | ```php 70 | FlysystemStreamWrapper::register('fly', $filesystem, [ 71 | FlysystemStreamWrapper::UID => 1000, 72 | FlysystemStreamWrapper::GID => 1000, 73 | ]); 74 | ``` 75 | 76 | Alternatively, access the parameters for [`PortableVisibilityConverter`](https://flysystem.thephpleague.com/docs/usage/unix-visibility/) directly: 77 | 78 | ```php 79 | FlysystemStreamWrapper::register('fly', $filesystem, [ 80 | FlysystemStreamWrapper::VISIBILITY_FILE_PUBLIC => 0644, 81 | FlysystemStreamWrapper::VISIBILITY_FILE_PRIVATE => 0600, 82 | FlysystemStreamWrapper::VISIBILITY_DIRECTORY_PUBLIC => 0755, 83 | FlysystemStreamWrapper::VISIBILITY_DIRECTORY_PRIVATE => 0700, 84 | FlysystemStreamWrapper::VISIBILITY_DEFAULT_FOR_DIRECTORIES => Visibility::PRIVATE, 85 | ]); 86 | ``` 87 | 88 | ## Testing 89 | 90 | This package was developed using PHP 7.4 and has been tested for compatibility with PHP versions 7.2 through 8.3. 91 | 92 | To test: 93 | 94 | - With PHP installed: 95 | ```bash 96 | composer test 97 | ``` 98 | 99 | - Inside a Docker environment for PHP 7.4: 100 | ```bash 101 | docker compose run php74 composer test 102 | ``` 103 | 104 | **Note**: PHPUnit v10, used from PHP 8.1 onwards, requires a different config file: 105 | ```bash 106 | docker compose run php81 composer test10 107 | ``` 108 | 109 | ## Changelog 110 | 111 | For information on recent changes, refer to the [CHANGELOG](CHANGELOG.md). 112 | 113 | ## Contributing 114 | 115 | For contribution guidelines, see [CONTRIBUTING](.github/CONTRIBUTING.md). 116 | 117 | ## Security Vulnerabilities 118 | 119 | If you discover any security vulnerabilities, please follow [our security policy](../../security/policy) for reporting. 120 | 121 | ## Credits 122 | 123 | - This package was inspired by [twistor/flysystem-stream-wrapper](https://github.com/twistor/flysystem-stream-wrapper). Many thanks to [Chris Leppanen](https://github.com/twistor). 124 | - [All Contributors](../../contributors) 125 | 126 | ## License 127 | 128 | Licensed under the MIT License. See the [License File](LICENSE.md) for more details. 129 | -------------------------------------------------------------------------------- /src/Flysystem/StreamCommand/StreamStatCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace M2MTech\FlysystemStreamWrapper\Flysystem\StreamCommand; 11 | 12 | use Iterator; 13 | use IteratorIterator; 14 | use League\Flysystem\FileAttributes; 15 | use League\Flysystem\FilesystemException; 16 | use League\Flysystem\UnableToRetrieveMetadata; 17 | use League\Flysystem\UnixVisibility\PortableVisibilityConverter; 18 | use League\Flysystem\Visibility; 19 | use M2MTech\FlysystemStreamWrapper\Flysystem\Exception\StatFailedException; 20 | use M2MTech\FlysystemStreamWrapper\Flysystem\FileData; 21 | use M2MTech\FlysystemStreamWrapper\FlysystemStreamWrapper; 22 | use TypeError; 23 | 24 | final class StreamStatCommand 25 | { 26 | use ExceptionHandler; 27 | 28 | public const STAT_COMMAND = 'stream_stat'; 29 | 30 | /** @return array|false */ 31 | public static function run(FileData $current) 32 | { 33 | try { 34 | return self::getStat($current); 35 | } catch (FilesystemException $e) { 36 | self::triggerError(StatFailedException::atLocation(self::STAT_COMMAND, $current->path, $e)); 37 | 38 | return false; 39 | } 40 | } 41 | 42 | private const STATS_ZERO = [0, 'dev', 1, 'ino', 3, 'nlink', 6, 'rdev']; 43 | private const STATS_MODE = [2, 'mode']; 44 | private const STATS_SIZE = [7, 'size']; 45 | private const STATS_TIME = [8, 'atime', 9, 'mtime', 10, 'ctime']; 46 | private const STATS_MINUS_ONE = [11, 'blksize', 12, 'blocks']; 47 | 48 | /** 49 | * @return array|false 50 | * 51 | * @throws FilesystemException 52 | */ 53 | public static function getStat(FileData $current) 54 | { 55 | $stats = []; 56 | 57 | if ($current->workOnLocalCopy && is_resource($current->handle)) { 58 | $stats = fstat($current->handle); 59 | if (!$stats) { 60 | return false; 61 | } 62 | 63 | if ($current->filesystem->fileExists($current->file)) { 64 | [$mode, $size, $time] = self::getRemoteStats($current); 65 | 66 | unset($size); 67 | } 68 | } else { 69 | [$mode, $size, $time] = self::getRemoteStats($current); 70 | } 71 | 72 | foreach (self::STATS_ZERO as $key) { 73 | $stats[$key] = 0; 74 | } 75 | 76 | foreach (self::STATS_MINUS_ONE as $key) { 77 | $stats[$key] = -1; 78 | } 79 | 80 | if (isset($mode)) { 81 | foreach (self::STATS_MODE as $key) { 82 | $stats[$key] = $mode; 83 | } 84 | } 85 | 86 | if (isset($size)) { 87 | foreach (self::STATS_SIZE as $key) { 88 | $stats[$key] = $size; 89 | } 90 | } 91 | 92 | if (isset($time)) { 93 | foreach (self::STATS_TIME as $key) { 94 | $stats[$key] = $time; 95 | } 96 | } 97 | 98 | $stats['uid'] = $stats[4] = (int) $current->config[FlysystemStreamWrapper::UID]; 99 | $stats['gid'] = $stats[5] = (int) $current->config[FlysystemStreamWrapper::GID]; 100 | 101 | return $stats; 102 | } 103 | 104 | /** 105 | * @throws FilesystemException 106 | * 107 | * @return array 108 | */ 109 | public static function getRemoteStats(FileData $current): array 110 | { 111 | $converter = new PortableVisibilityConverter( 112 | (int) $current->config[FlysystemStreamWrapper::VISIBILITY_FILE_PUBLIC], 113 | (int) $current->config[FlysystemStreamWrapper::VISIBILITY_FILE_PRIVATE], 114 | (int) $current->config[FlysystemStreamWrapper::VISIBILITY_DIRECTORY_PUBLIC], 115 | (int) $current->config[FlysystemStreamWrapper::VISIBILITY_DIRECTORY_PRIVATE], 116 | (string) $current->config[FlysystemStreamWrapper::VISIBILITY_DEFAULT_FOR_DIRECTORIES] 117 | ); 118 | 119 | try { 120 | $visibility = $current->filesystem->visibility($current->file); 121 | } catch (UnableToRetrieveMetadata | TypeError $e) { 122 | if (!$current->ignoreVisibilityErrors()) { 123 | throw $e; 124 | } 125 | 126 | $visibility = Visibility::PUBLIC; 127 | } 128 | 129 | $mode = 0; 130 | $size = 0; 131 | $lastModified = 0; 132 | 133 | try { 134 | if ('directory' === $current->filesystem->mimeType($current->file)) { 135 | [$mode, $size, $lastModified] = self::getRemoteDirectoryStats($current, $converter, $visibility); 136 | } else { 137 | [$mode, $size, $lastModified] = self::getRemoteFileStats($current, $converter, $visibility); 138 | } 139 | } catch (UnableToRetrieveMetadata $e) { 140 | if (method_exists($current->filesystem, 'directoryExists')) { 141 | if ($current->filesystem->directoryExists($current->file)) { 142 | [$mode, $size, $lastModified] = self::getRemoteDirectoryStats($current, $converter, $visibility); 143 | } elseif ($current->filesystem->fileExists($current->file)) { 144 | [$mode, $size, $lastModified] = self::getRemoteFileStats($current, $converter, $visibility); 145 | } 146 | } else { 147 | throw $e; 148 | } 149 | } 150 | 151 | return [$mode, $size, $lastModified]; 152 | } 153 | 154 | /** 155 | * @return array 156 | * 157 | * @throws FilesystemException 158 | */ 159 | private static function getRemoteDirectoryStats( 160 | FileData $current, 161 | PortableVisibilityConverter $converter, 162 | string $visibility 163 | ): array { 164 | $mode = 040000 + $converter->forDirectory($visibility); 165 | $size = 0; 166 | 167 | $lastModified = self::getRemoteDirectoryLastModified($current); 168 | 169 | return [$mode, $size, $lastModified]; 170 | } 171 | 172 | /** 173 | * @return array 174 | * 175 | * @throws FilesystemException 176 | */ 177 | private static function getRemoteFileStats( 178 | FileData $current, 179 | PortableVisibilityConverter $converter, 180 | string $visibility 181 | ): array { 182 | $mode = 0100000 + $converter->forFile($visibility); 183 | $size = $current->filesystem->fileSize($current->file); 184 | $lastModified = $current->filesystem->lastModified($current->file); 185 | 186 | return [$mode, $size, $lastModified]; 187 | } 188 | 189 | /** 190 | * @throws FilesystemException 191 | */ 192 | private static function getRemoteDirectoryLastModified(FileData $current): int 193 | { 194 | if (!$current->emulateDirectoryLastModified()) { 195 | return $current->filesystem->lastModified($current->file); 196 | } 197 | 198 | $lastModified = 0; 199 | $listing = $current->filesystem->listContents($current->file)->getIterator(); 200 | $dirListing = $listing instanceof Iterator ? $listing : new IteratorIterator($listing); 201 | 202 | /** @var FileAttributes $item */ 203 | foreach ($dirListing as $item) { 204 | $lastModified = max($lastModified, $item->lastModified()); 205 | } 206 | 207 | return $lastModified; 208 | } 209 | } 210 | --------------------------------------------------------------------------------