├── .appveyor.yml ├── LICENSE ├── composer.json └── src ├── Flysystem ├── Exception │ ├── DirectoryExistsException.php │ ├── DirectoryNotEmptyException.php │ ├── NotADirectoryException.php │ └── TriggerErrorException.php └── Plugin │ ├── AbstractPlugin.php │ ├── ForcedRename.php │ ├── Mkdir.php │ ├── Rmdir.php │ ├── Stat.php │ └── Touch.php ├── FlysystemStreamWrapper.php ├── PosixUid.php └── Uid.php /.appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | 3 | platform: 4 | - x64 5 | 6 | clone_folder: c:\projects\project-code 7 | 8 | cache: 9 | - c:\ProgramData\chocolatey\bin -> .appveyor.yml 10 | - c:\ProgramData\chocolatey\lib -> .appveyor.yml 11 | - c:\ProgramData\ComposerSetup -> .appveyor.yml 12 | - c:\tools\php -> .appveyor.yml 13 | 14 | environment: 15 | global: 16 | COMPOSER_NO_INTERACTION: 1 17 | ANSICON: 121x90 (121x90) # Console colors 18 | 19 | matrix: 20 | - PHP_VERSION: 5.6 21 | COMPOSER_OPTS: --prefer-lowest 22 | 23 | - PHP_VERSION: 5.6 24 | COMPOSER_OPTS: 25 | 26 | - PHP_VERSION: 7.2 27 | COMPOSER_OPTS: --prefer-lowest 28 | 29 | - PHP_VERSION: 7.2 30 | COMPOSER_OPTS: 31 | 32 | init: 33 | - ps: $env:PATH = 'c:\tools\php;c:\ProgramData\ComposerSetup\bin;' + $env:PATH 34 | 35 | install: 36 | - ps: Set-Service wuauserv -StartupType Manual # Chocolatey will try to install Windows updates when installing PHP. 37 | - ps: appveyor-retry cinst --no-progress --params '""/InstallDir:c:\tools\php""' -y php --version ((choco search php --exact --all-versions -r | select-string -pattern $env:PHP_VERSION | sort { [version]($_ -split '\|' | select -last 1) } -Descending | Select-Object -first 1) -replace '[php|]','') 38 | 39 | - cd c:\tools\php 40 | - copy php.ini-production php.ini /Y 41 | - echo date.timezone="UTC" >> php.ini 42 | - echo extension_dir=ext >> php.ini 43 | - echo extension=php_openssl.dll >> php.ini # Needed to install Composer 44 | - echo extension=php_fileinfo.dll >> php.ini 45 | - appveyor-retry cinst --no-progress -y composer 46 | 47 | - cd c:\projects\project-code 48 | - appveyor-retry composer self-update 49 | - appveyor-retry composer update --no-interaction --no-progress --no-suggest --optimize-autoloader --prefer-stable --prefer-dist %COMPOSER_OPTS% 50 | 51 | test_script: 52 | - cd c:\projects\project-code 53 | - vendor\bin\phpunit --verbose 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chris Leppanen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twistor/flysystem-stream-wrapper", 3 | "type": "library", 4 | "description": "Adapts Flysystem filesystems to PHP stream wrappers.", 5 | "license": "MIT", 6 | "homepage": "http://github.com/twistor/flysystem-stream-wrapper", 7 | "authors": [ 8 | { 9 | "name": "Chris Leppanen", 10 | "email": "chris.leppanen@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "league/flysystem": "^1.0.9", 15 | "twistor/stream-util": "~1.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "~4.8" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Twistor\\": "src/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Flysystem/Exception/DirectoryExistsException.php: -------------------------------------------------------------------------------- 1 | message ? $this->message : $this->defaultMessage, $function); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Flysystem/Plugin/AbstractPlugin.php: -------------------------------------------------------------------------------- 1 | setFallback($this->filesystem->getConfig()); 19 | 20 | return $config; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Flysystem/Plugin/ForcedRename.php: -------------------------------------------------------------------------------- 1 | isValidRename($path, $newpath)) { 45 | // Returns false if a Flysystem call fails. 46 | return false; 47 | } 48 | 49 | return (bool) $this->filesystem->getAdapter()->rename($path, $newpath); 50 | } 51 | 52 | /** 53 | * Checks that a rename is valid. 54 | * 55 | * @param string $source 56 | * @param string $dest 57 | * 58 | * @return bool 59 | * 60 | * @throws \League\Flysystem\FileNotFoundException 61 | * @throws \Twistor\Flysystem\Exception\DirectoryExistsException 62 | * @throws \Twistor\Flysystem\Exception\DirectoryNotEmptyException 63 | * @throws \Twistor\Flysystem\Exception\NotADirectoryException 64 | */ 65 | protected function isValidRename($source, $dest) 66 | { 67 | $adapter = $this->filesystem->getAdapter(); 68 | 69 | if ( ! $adapter->has($source)) { 70 | throw new FileNotFoundException($source); 71 | } 72 | 73 | $subdir = Util::dirname($dest); 74 | 75 | if (strlen($subdir) && ! $adapter->has($subdir)) { 76 | throw new FileNotFoundException($source); 77 | } 78 | 79 | if ( ! $adapter->has($dest)) { 80 | return true; 81 | } 82 | 83 | return $this->compareTypes($source, $dest); 84 | } 85 | 86 | /** 87 | * Compares the file/dir for the source and dest. 88 | * 89 | * @param string $source 90 | * @param string $dest 91 | * 92 | * @return bool 93 | * 94 | * @throws \Twistor\Flysystem\Exception\DirectoryExistsException 95 | * @throws \Twistor\Flysystem\Exception\DirectoryNotEmptyException 96 | * @throws \Twistor\Flysystem\Exception\NotADirectoryException 97 | */ 98 | protected function compareTypes($source, $dest) 99 | { 100 | $adapter = $this->filesystem->getAdapter(); 101 | 102 | $source_type = $adapter->getMetadata($source)['type']; 103 | $dest_type = $adapter->getMetadata($dest)['type']; 104 | 105 | // These three checks are done in order of cost to minimize Flysystem 106 | // calls. 107 | 108 | // Don't allow overwriting different types. 109 | if ($source_type !== $dest_type) { 110 | if ($dest_type === 'dir') { 111 | throw new DirectoryExistsException(); 112 | } 113 | 114 | throw new NotADirectoryException(); 115 | } 116 | 117 | // Allow overwriting destination file. 118 | if ($source_type === 'file') { 119 | return $adapter->delete($dest); 120 | } 121 | 122 | // Allow overwriting destination directory if not empty. 123 | $contents = $this->filesystem->listContents($dest); 124 | if ( ! empty($contents)) { 125 | throw new DirectoryNotEmptyException(); 126 | } 127 | 128 | return $adapter->deleteDir($dest); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Flysystem/Plugin/Mkdir.php: -------------------------------------------------------------------------------- 1 | filesystem->getAdapter(); 34 | 35 | // If recursive, or a single level directory, just create it. 36 | if (($options & STREAM_MKDIR_RECURSIVE) || strpos($dirname, '/') === false) { 37 | return (bool) $adapter->createDir($dirname, $this->defaultConfig()); 38 | } 39 | 40 | if ( ! $adapter->has(dirname($dirname))) { 41 | throw new FileNotFoundException($dirname); 42 | } 43 | 44 | return (bool) $adapter->createDir($dirname, $this->defaultConfig()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Flysystem/Plugin/Rmdir.php: -------------------------------------------------------------------------------- 1 | filesystem->getAdapter(); 38 | 39 | if ($options & STREAM_MKDIR_RECURSIVE) { 40 | // I don't know how this gets triggered. 41 | return (bool) $adapter->deleteDir($dirname); 42 | } 43 | 44 | $contents = $this->filesystem->listContents($dirname); 45 | 46 | if ( ! empty($contents)) { 47 | throw new DirectoryNotEmptyException(); 48 | } 49 | 50 | return (bool) $adapter->deleteDir($dirname); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Flysystem/Plugin/Stat.php: -------------------------------------------------------------------------------- 1 | 0, 19 | 'ino' => 0, 20 | 'mode' => 0, 21 | 'nlink' => 0, 22 | 'uid' => 0, 23 | 'gid' => 0, 24 | 'rdev' => 0, 25 | 'size' => 0, 26 | 'atime' => 0, 27 | 'mtime' => 0, 28 | 'ctime' => 0, 29 | 'blksize' => -1, 30 | 'blocks' => -1, 31 | ]; 32 | 33 | /** 34 | * Permission map. 35 | * 36 | * @var array 37 | */ 38 | protected $permissions; 39 | 40 | /** 41 | * Required metadata. 42 | * 43 | * @var array 44 | */ 45 | protected $required; 46 | 47 | /** 48 | * @var \Twistor\Uid 49 | */ 50 | protected $uid; 51 | 52 | /** 53 | * Constructs a Stat object. 54 | * 55 | * @param array $permissions An array of permissions. 56 | * @param array $metadata The default required metadata. 57 | */ 58 | public function __construct(array $permissions, array $metadata) 59 | { 60 | $this->permissions = $permissions; 61 | $this->required = array_combine($metadata, $metadata); 62 | $this->uid = \extension_loaded('posix') ? new PosixUid() : new Uid(); 63 | } 64 | 65 | /** 66 | * @inheritdoc 67 | */ 68 | public function getMethod() 69 | { 70 | return 'stat'; 71 | } 72 | 73 | /** 74 | * Emulates stat(). 75 | * 76 | * @param string $path 77 | * @param int $flags 78 | * 79 | * @return array Output similar to stat(). 80 | * 81 | * @throws \League\Flysystem\FileNotFoundException 82 | * 83 | * @see stat() 84 | */ 85 | public function handle($path, $flags) 86 | { 87 | if ($path === '') { 88 | return $this->mergeMeta(['type' => 'dir', 'visibility' => AdapterInterface::VISIBILITY_PUBLIC]); 89 | } 90 | 91 | $ignore = $flags & FlysystemStreamWrapper::STREAM_URL_IGNORE_SIZE ? ['size'] : []; 92 | 93 | $metadata = $this->getWithMetadata($path, $ignore); 94 | 95 | // It's possible for getMetadata() to fail even if a file exists. 96 | if (empty($metadata)) { 97 | return static::$defaultMeta; 98 | } 99 | 100 | return $this->mergeMeta($metadata + ['visibility' => AdapterInterface::VISIBILITY_PUBLIC]); 101 | } 102 | 103 | /** 104 | * Returns metadata. 105 | * 106 | * @param string $path The path to get metadata for. 107 | * @param array $ignore Metadata to ignore. 108 | * 109 | * @return array The metadata as returned by Filesystem::getMetadata(). 110 | * 111 | * @throws \League\Flysystem\FileNotFoundException 112 | * 113 | * @see \League\Flysystem\Filesystem::getMetadata() 114 | */ 115 | protected function getWithMetadata($path, array $ignore) 116 | { 117 | $metadata = $this->filesystem->getMetadata($path); 118 | 119 | if (empty($metadata)) { 120 | return []; 121 | } 122 | 123 | $keys = array_diff($this->required, array_keys($metadata), $ignore); 124 | 125 | foreach ($keys as $key) { 126 | $method = 'get' . ucfirst($key); 127 | 128 | try { 129 | $metadata[$key] = $this->filesystem->$method($path); 130 | } catch (\LogicException $e) { 131 | // Some adapters don't support certain metadata. For instance, 132 | // the Dropbox adapter throws exceptions when calling 133 | // getVisibility(). Remove the required key so we don't keep 134 | // calling it. 135 | unset($this->required[$key]); 136 | } 137 | } 138 | 139 | return $metadata; 140 | } 141 | 142 | /** 143 | * Merges the available metadata from Filesystem::getMetadata(). 144 | * 145 | * @param array $metadata The metadata. 146 | * 147 | * @return array All metadata with default values filled in. 148 | */ 149 | protected function mergeMeta(array $metadata) 150 | { 151 | $ret = static::$defaultMeta; 152 | 153 | $ret['uid'] = $this->uid->getUid(); 154 | $ret['gid'] = $this->uid->getGid(); 155 | 156 | $ret['mode'] = $metadata['type'] === 'dir' ? 040000 : 0100000; 157 | $ret['mode'] += $this->permissions[$metadata['type']][$metadata['visibility']]; 158 | 159 | if (isset($metadata['size'])) { 160 | $ret['size'] = (int) $metadata['size']; 161 | } 162 | if (isset($metadata['timestamp'])) { 163 | $ret['mtime'] = (int) $metadata['timestamp']; 164 | $ret['ctime'] = (int) $metadata['timestamp']; 165 | } 166 | 167 | $ret['atime'] = time(); 168 | 169 | return array_merge(array_values($ret), $ret); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Flysystem/Plugin/Touch.php: -------------------------------------------------------------------------------- 1 | filesystem->getAdapter(); 29 | 30 | if ($adapter->has($path)) { 31 | return true; 32 | } 33 | 34 | return (bool) $adapter->write($path, '', $this->defaultConfig()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/FlysystemStreamWrapper.php: -------------------------------------------------------------------------------- 1 | [ 49 | 'dir' => [ 50 | 'private' => 0700, 51 | 'public' => 0755, 52 | ], 53 | 'file' => [ 54 | 'private' => 0600, 55 | 'public' => 0644, 56 | ], 57 | ], 58 | 'metadata' => ['timestamp', 'size', 'visibility'], 59 | 'public_mask' => 0044, 60 | ]; 61 | 62 | /** 63 | * The number of bytes that have been written since the last flush. 64 | * 65 | * @var int 66 | */ 67 | protected $bytesWritten = 0; 68 | 69 | /** 70 | * The filesystem of the current stream wrapper. 71 | * 72 | * @var \League\Flysystem\FilesystemInterface 73 | */ 74 | protected $filesystem; 75 | 76 | /** 77 | * A generic resource handle. 78 | * 79 | * @var resource|bool 80 | */ 81 | protected $handle; 82 | 83 | /** 84 | * Whether the handle is in append mode. 85 | * 86 | * @var bool 87 | */ 88 | protected $isAppendMode = false; 89 | 90 | /** 91 | * Whether the handle is read-only. 92 | * 93 | * The stream returned from Flysystem may not actually be read-only, This 94 | * ensures read-only behavior. 95 | * 96 | * @var bool 97 | */ 98 | protected $isReadOnly = false; 99 | 100 | /** 101 | * Whether the handle is write-only. 102 | * 103 | * @var bool 104 | */ 105 | protected $isWriteOnly = false; 106 | 107 | /** 108 | * A directory listing. 109 | * 110 | * @var array 111 | */ 112 | protected $listing; 113 | 114 | /** 115 | * Whether this handle has been verified writable. 116 | * 117 | * @var bool 118 | */ 119 | protected $needsCowCheck = false; 120 | 121 | /** 122 | * Whether the handle should be flushed. 123 | * 124 | * @var bool 125 | */ 126 | protected $needsFlush = false; 127 | 128 | /** 129 | * The handle used for calls to stream_lock. 130 | * 131 | * @var resource 132 | */ 133 | protected $lockHandle; 134 | 135 | /** 136 | * If stream_set_write_buffer() is called, the arguments. 137 | * 138 | * @var int 139 | */ 140 | protected $streamWriteBuffer; 141 | 142 | /** 143 | * Instance URI (stream). 144 | * 145 | * A stream is referenced as "protocol://target". 146 | * 147 | * @var string 148 | */ 149 | protected $uri; 150 | 151 | /** 152 | * Registers the stream wrapper protocol if not already registered. 153 | * 154 | * @param string $protocol The protocol. 155 | * @param FilesystemInterface $filesystem The filesystem. 156 | * @param array|null $configuration Optional configuration. 157 | * @param int $flags Should be set to STREAM_IS_URL if protocol is a URL protocol. Default is 0, local stream. 158 | * 159 | * @return bool True if the protocol was registered, false if not. 160 | */ 161 | public static function register($protocol, FilesystemInterface $filesystem, array $configuration = null, $flags = 0) 162 | { 163 | if (static::streamWrapperExists($protocol)) { 164 | return false; 165 | } 166 | 167 | static::$config[$protocol] = $configuration ?: static::$defaultConfiguration; 168 | static::registerPlugins($protocol, $filesystem); 169 | static::$filesystems[$protocol] = $filesystem; 170 | 171 | return stream_wrapper_register($protocol, __CLASS__, $flags); 172 | } 173 | 174 | /** 175 | * Unregisters a stream wrapper. 176 | * 177 | * @param string $protocol The protocol. 178 | * 179 | * @return bool True if the protocol was unregistered, false if not. 180 | */ 181 | public static function unregister($protocol) 182 | { 183 | if ( ! static::streamWrapperExists($protocol)) { 184 | return false; 185 | } 186 | 187 | unset(static::$filesystems[$protocol]); 188 | 189 | return stream_wrapper_unregister($protocol); 190 | } 191 | 192 | /** 193 | * Unregisters all controlled stream wrappers. 194 | */ 195 | public static function unregisterAll() 196 | { 197 | foreach (static::getRegisteredProtocols() as $protocol) { 198 | static::unregister($protocol); 199 | } 200 | } 201 | 202 | /** 203 | * @return array The list of registered protocols. 204 | */ 205 | public static function getRegisteredProtocols() 206 | { 207 | return array_keys(static::$filesystems); 208 | } 209 | 210 | /** 211 | * Determines if a protocol is registered. 212 | * 213 | * @param string $protocol The protocol to check. 214 | * 215 | * @return bool True if it is registered, false if not. 216 | */ 217 | protected static function streamWrapperExists($protocol) 218 | { 219 | return in_array($protocol, stream_get_wrappers(), true); 220 | } 221 | 222 | /** 223 | * Registers plugins on the filesystem. 224 | * @param string $protocol 225 | * @param FilesystemInterface $filesystem 226 | */ 227 | protected static function registerPlugins($protocol, FilesystemInterface $filesystem) 228 | { 229 | $filesystem->addPlugin(new ForcedRename()); 230 | $filesystem->addPlugin(new Mkdir()); 231 | $filesystem->addPlugin(new Rmdir()); 232 | 233 | $stat = new Stat( 234 | static::$config[$protocol]['permissions'], 235 | static::$config[$protocol]['metadata'] 236 | ); 237 | 238 | $filesystem->addPlugin($stat); 239 | $filesystem->addPlugin(new Touch()); 240 | } 241 | 242 | /** 243 | * Closes the directory handle. 244 | * 245 | * @return bool True on success, false on failure. 246 | */ 247 | public function dir_closedir() 248 | { 249 | unset($this->listing); 250 | 251 | return true; 252 | } 253 | 254 | /** 255 | * Opens a directory handle. 256 | * 257 | * @param string $uri The URL that was passed to opendir(). 258 | * @param int $options Whether or not to enforce safe_mode (0x04). 259 | * 260 | * @return bool True on success, false on failure. 261 | */ 262 | public function dir_opendir($uri, $options) 263 | { 264 | $this->uri = $uri; 265 | 266 | $path = Util::normalizePath($this->getTarget()); 267 | 268 | $this->listing = $this->invoke($this->getFilesystem(), 'listContents', [$path], 'opendir'); 269 | 270 | if ($this->listing === false) { 271 | return false; 272 | } 273 | 274 | if ( ! $dirlen = strlen($path)) { 275 | return true; 276 | } 277 | 278 | // Remove the separator /. 279 | $dirlen++; 280 | 281 | // Remove directory prefix. 282 | foreach ($this->listing as $delta => $item) { 283 | $this->listing[$delta]['path'] = substr($item['path'], $dirlen); 284 | } 285 | 286 | reset($this->listing); 287 | 288 | return true; 289 | } 290 | 291 | /** 292 | * Reads an entry from directory handle. 293 | * 294 | * @return string|bool The next filename, or false if there is no next file. 295 | */ 296 | public function dir_readdir() 297 | { 298 | $current = current($this->listing); 299 | next($this->listing); 300 | 301 | return $current ? $current['path'] : false; 302 | } 303 | 304 | /** 305 | * Rewinds the directory handle. 306 | * 307 | * @return bool True on success, false on failure. 308 | */ 309 | public function dir_rewinddir() 310 | { 311 | reset($this->listing); 312 | 313 | return true; 314 | } 315 | 316 | /** 317 | * Creates a directory. 318 | * 319 | * @param string $uri 320 | * @param int $mode 321 | * @param int $options 322 | * 323 | * @return bool True on success, false on failure. 324 | */ 325 | public function mkdir($uri, $mode, $options) 326 | { 327 | $this->uri = $uri; 328 | 329 | return $this->invoke($this->getFilesystem(), 'mkdir', [$this->getTarget(), $mode, $options]); 330 | } 331 | 332 | /** 333 | * Renames a file or directory. 334 | * 335 | * @param string $uri_from 336 | * @param string $uri_to 337 | * 338 | * @return bool True on success, false on failure. 339 | */ 340 | public function rename($uri_from, $uri_to) 341 | { 342 | $this->uri = $uri_from; 343 | $args = [$this->getTarget($uri_from), $this->getTarget($uri_to)]; 344 | 345 | return $this->invoke($this->getFilesystem(), 'forcedRename', $args, 'rename'); 346 | } 347 | 348 | /** 349 | * Removes a directory. 350 | * 351 | * @param string $uri 352 | * @param int $options 353 | * 354 | * @return bool True on success, false on failure. 355 | */ 356 | public function rmdir($uri, $options) 357 | { 358 | $this->uri = $uri; 359 | 360 | return $this->invoke($this->getFilesystem(), 'rmdir', [$this->getTarget(), $options]); 361 | } 362 | 363 | /** 364 | * Retrieves the underlying resource. 365 | * 366 | * @param int $cast_as 367 | * 368 | * @return resource|bool The stream resource used by the wrapper, or false. 369 | */ 370 | public function stream_cast($cast_as) 371 | { 372 | return $this->handle; 373 | } 374 | 375 | /** 376 | * Closes the resource. 377 | */ 378 | public function stream_close() 379 | { 380 | // PHP 7 doesn't call flush automatically anymore for truncate() or when 381 | // writing an empty file. We need to ensure that the handle gets pushed 382 | // as needed in that case. This will be a no-op for php 5. 383 | $this->stream_flush(); 384 | 385 | if (is_resource($this->handle)) { 386 | fclose($this->handle); 387 | } 388 | } 389 | 390 | /** 391 | * Tests for end-of-file on a file pointer. 392 | * 393 | * @return bool True if the file is at the end, false if not. 394 | */ 395 | public function stream_eof() 396 | { 397 | return feof($this->handle); 398 | } 399 | 400 | /** 401 | * Flushes the output. 402 | * 403 | * @return bool True on success, false on failure. 404 | */ 405 | public function stream_flush() 406 | { 407 | if ( ! $this->needsFlush) { 408 | return true; 409 | } 410 | 411 | $this->needsFlush = false; 412 | $this->bytesWritten = 0; 413 | 414 | // Calling putStream() will rewind our handle. flush() shouldn't change 415 | // the position of the file. 416 | $pos = ftell($this->handle); 417 | 418 | $args = [$this->getTarget(), $this->handle]; 419 | $success = $this->invoke($this->getFilesystem(), 'putStream', $args, 'fflush'); 420 | 421 | if (is_resource($this->handle)) { 422 | fseek($this->handle, $pos); 423 | } 424 | 425 | return $success; 426 | } 427 | 428 | /** 429 | * Advisory file locking. 430 | * 431 | * @param int $operation 432 | * 433 | * @return bool True on success, false on failure. 434 | */ 435 | public function stream_lock($operation) 436 | { 437 | $operation = (int) $operation; 438 | 439 | if (($operation & \LOCK_UN) === \LOCK_UN) { 440 | return $this->releaseLock($operation); 441 | } 442 | 443 | // If the caller calls flock() twice, there's no reason to re-create the 444 | // lock handle. 445 | if (is_resource($this->lockHandle)) { 446 | return flock($this->lockHandle, $operation); 447 | } 448 | 449 | $this->lockHandle = $this->openLockHandle(); 450 | 451 | return is_resource($this->lockHandle) && flock($this->lockHandle, $operation); 452 | } 453 | 454 | /** 455 | * Changes stream options. 456 | * 457 | * @param string $uri 458 | * @param int $option 459 | * @param mixed $value 460 | * 461 | * @return bool True on success, false on failure. 462 | */ 463 | public function stream_metadata($uri, $option, $value) 464 | { 465 | $this->uri = $uri; 466 | 467 | switch ($option) { 468 | case STREAM_META_ACCESS: 469 | $permissions = octdec(substr(decoct($value), -4)); 470 | $is_public = $permissions & $this->getConfiguration('public_mask'); 471 | $visibility = $is_public ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE; 472 | 473 | try { 474 | return $this->getFilesystem()->setVisibility($this->getTarget(), $visibility); 475 | } catch (\LogicException $e) { 476 | // The adapter doesn't support visibility. 477 | } catch (\Exception $e) { 478 | $this->triggerError('chmod', $e); 479 | 480 | return false; 481 | } 482 | 483 | return true; 484 | 485 | case STREAM_META_TOUCH: 486 | return $this->invoke($this->getFilesystem(), 'touch', [$this->getTarget()]); 487 | 488 | default: 489 | return false; 490 | } 491 | } 492 | 493 | /** 494 | * Opens file or URL. 495 | * 496 | * @param string $uri 497 | * @param string $mode 498 | * @param int $options 499 | * @param string &$opened_path 500 | * 501 | * @return bool True on success, false on failure. 502 | */ 503 | public function stream_open($uri, $mode, $options, &$opened_path) 504 | { 505 | $this->uri = $uri; 506 | $path = $this->getTarget(); 507 | 508 | $this->isReadOnly = StreamUtil::modeIsReadOnly($mode); 509 | $this->isWriteOnly = StreamUtil::modeIsWriteOnly($mode); 510 | $this->isAppendMode = StreamUtil::modeIsAppendable($mode); 511 | 512 | $this->handle = $this->invoke($this, 'getStream', [$path, $mode], 'fopen'); 513 | 514 | if ($this->handle && $options & STREAM_USE_PATH) { 515 | $opened_path = $path; 516 | } 517 | 518 | return is_resource($this->handle); 519 | } 520 | 521 | /** 522 | * Reads from stream. 523 | * 524 | * @param int $count 525 | * 526 | * @return string The bytes read. 527 | */ 528 | public function stream_read($count) 529 | { 530 | if ($this->isWriteOnly) { 531 | return ''; 532 | } 533 | 534 | return fread($this->handle, $count); 535 | } 536 | 537 | /** 538 | * Seeks to specific location in a stream. 539 | * 540 | * @param int $offset 541 | * @param int $whence 542 | * 543 | * @return bool True on success, false on failure. 544 | */ 545 | public function stream_seek($offset, $whence = SEEK_SET) 546 | { 547 | return fseek($this->handle, $offset, $whence) === 0; 548 | } 549 | 550 | /** 551 | * Changes stream options. 552 | * 553 | * @param int $option 554 | * @param int $arg1 555 | * @param int $arg2 556 | * 557 | * @return bool True on success, false on failure. 558 | */ 559 | public function stream_set_option($option, $arg1, $arg2) 560 | { 561 | switch ($option) { 562 | case STREAM_OPTION_BLOCKING: 563 | // This works for the local adapter. It doesn't do anything for 564 | // memory streams. 565 | return stream_set_blocking($this->handle, $arg1); 566 | 567 | case STREAM_OPTION_READ_TIMEOUT: 568 | return stream_set_timeout($this->handle, $arg1, $arg2); 569 | 570 | case STREAM_OPTION_READ_BUFFER: 571 | if ($arg1 === STREAM_BUFFER_NONE) { 572 | return stream_set_read_buffer($this->handle, 0) === 0; 573 | } 574 | 575 | return stream_set_read_buffer($this->handle, $arg2) === 0; 576 | 577 | case STREAM_OPTION_WRITE_BUFFER: 578 | $this->streamWriteBuffer = $arg1 === STREAM_BUFFER_NONE ? 0 : $arg2; 579 | 580 | return true; 581 | } 582 | 583 | return false; 584 | } 585 | 586 | /** 587 | * Retrieves information about a file resource. 588 | * 589 | * @return array A similar array to fstat(). 590 | * 591 | * @see fstat() 592 | */ 593 | public function stream_stat() 594 | { 595 | // Get metadata from original file. 596 | $stat = $this->url_stat($this->uri, static::STREAM_URL_IGNORE_SIZE | STREAM_URL_STAT_QUIET) ?: []; 597 | 598 | // Newly created file. 599 | if (empty($stat['mode'])) { 600 | $stat['mode'] = 0100000 + $this->getConfiguration('permissions')['file']['public']; 601 | $stat[2] = $stat['mode']; 602 | } 603 | 604 | // Use the size of our handle, since it could have been written to or 605 | // truncated. 606 | $stat['size'] = $stat[7] = StreamUtil::getSize($this->handle); 607 | 608 | return $stat; 609 | } 610 | 611 | /** 612 | * Retrieves the current position of a stream. 613 | * 614 | * @return int The current position of the stream. 615 | */ 616 | public function stream_tell() 617 | { 618 | if ($this->isAppendMode) { 619 | return 0; 620 | } 621 | 622 | return ftell($this->handle); 623 | } 624 | 625 | /** 626 | * Truncates the stream. 627 | * 628 | * @param int $new_size 629 | * 630 | * @return bool True on success, false on failure. 631 | */ 632 | public function stream_truncate($new_size) 633 | { 634 | if ($this->isReadOnly) { 635 | return false; 636 | } 637 | $this->needsFlush = true; 638 | $this->ensureWritableHandle(); 639 | 640 | return ftruncate($this->handle, $new_size); 641 | } 642 | 643 | /** 644 | * Writes to the stream. 645 | * 646 | * @param string $data 647 | * 648 | * @return int The number of bytes that were successfully stored. 649 | */ 650 | public function stream_write($data) 651 | { 652 | if ($this->isReadOnly) { 653 | return 0; 654 | } 655 | $this->needsFlush = true; 656 | $this->ensureWritableHandle(); 657 | 658 | // Enforce append semantics. 659 | if ($this->isAppendMode) { 660 | StreamUtil::trySeek($this->handle, 0, SEEK_END); 661 | } 662 | 663 | $written = fwrite($this->handle, $data); 664 | $this->bytesWritten += $written; 665 | 666 | if (isset($this->streamWriteBuffer) && $this->bytesWritten >= $this->streamWriteBuffer) { 667 | $this->stream_flush(); 668 | } 669 | 670 | return $written; 671 | } 672 | 673 | /** 674 | * Deletes a file. 675 | * 676 | * @param string $uri 677 | * 678 | * @return bool True on success, false on failure. 679 | */ 680 | public function unlink($uri) 681 | { 682 | $this->uri = $uri; 683 | 684 | return $this->invoke($this->getFilesystem(), 'delete', [$this->getTarget()], 'unlink'); 685 | } 686 | 687 | /** 688 | * Retrieves information about a file. 689 | * 690 | * @param string $uri 691 | * @param int $flags 692 | * 693 | * @return array|false Output similar to stat(). 694 | * 695 | * @see stat() 696 | */ 697 | public function url_stat($uri, $flags) 698 | { 699 | $this->uri = $uri; 700 | 701 | try { 702 | return $this->getFilesystem()->stat($this->getTarget(), $flags); 703 | } catch (FileNotFoundException $e) { 704 | // File doesn't exist. 705 | if ( ! ($flags & STREAM_URL_STAT_QUIET)) { 706 | $this->triggerError('stat', $e); 707 | } 708 | } catch (\Exception $e) { 709 | $this->triggerError('stat', $e); 710 | } 711 | 712 | return false; 713 | } 714 | 715 | /** 716 | * Returns a stream for a given path and mode. 717 | * 718 | * @param string $path The path to open. 719 | * @param string $mode The mode to open the stream in. 720 | * 721 | * @return resource|bool The file handle, or false. 722 | * 723 | * @throws \League\Flysystem\FileNotFoundException 724 | */ 725 | protected function getStream($path, $mode) 726 | { 727 | switch ($mode[0]) { 728 | case 'r': 729 | $this->needsCowCheck = true; 730 | 731 | return $this->getFilesystem()->readStream($path); 732 | 733 | case 'w': 734 | $this->needsFlush = true; 735 | 736 | return fopen('php://temp', 'w+b'); 737 | 738 | case 'a': 739 | return $this->getAppendStream($path); 740 | 741 | case 'x': 742 | return $this->getXStream($path); 743 | 744 | case 'c': 745 | return $this->getWritableStream($path); 746 | } 747 | 748 | return false; 749 | } 750 | 751 | /** 752 | * Returns a writable stream for a given path and mode. 753 | * 754 | * @param string $path The path to open. 755 | * 756 | * @return resource|bool The file handle, or false. 757 | */ 758 | protected function getWritableStream($path) 759 | { 760 | try { 761 | $handle = $this->getFilesystem()->readStream($path); 762 | $this->needsCowCheck = true; 763 | } catch (FileNotFoundException $e) { 764 | $handle = fopen('php://temp', 'w+b'); 765 | $this->needsFlush = true; 766 | } 767 | 768 | return $handle; 769 | } 770 | 771 | /** 772 | * Returns an appendable stream for a given path and mode. 773 | * 774 | * @param string $path The path to open. 775 | * 776 | * @return resource|bool The file handle, or false. 777 | */ 778 | protected function getAppendStream($path) 779 | { 780 | if ($handle = $this->getWritableStream($path)) { 781 | StreamUtil::trySeek($handle, 0, SEEK_END); 782 | } 783 | 784 | return $handle; 785 | } 786 | 787 | /** 788 | * Returns a writable stream for a given path and mode. 789 | * 790 | * Triggers a warning if the file exists. 791 | * 792 | * @param string $path The path to open. 793 | * 794 | * @return resource|bool The file handle, or false. 795 | */ 796 | protected function getXStream($path) 797 | { 798 | if ($this->getFilesystem()->has($path)) { 799 | trigger_error('fopen(): failed to open stream: File exists', E_USER_WARNING); 800 | 801 | return false; 802 | } 803 | 804 | $this->needsFlush = true; 805 | 806 | return fopen('php://temp', 'w+b'); 807 | } 808 | 809 | /** 810 | * Guarantees that the handle is writable. 811 | */ 812 | protected function ensureWritableHandle() 813 | { 814 | if ( ! $this->needsCowCheck) { 815 | return; 816 | } 817 | 818 | $this->needsCowCheck = false; 819 | 820 | if (StreamUtil::isWritable($this->handle)) { 821 | return; 822 | } 823 | 824 | $this->handle = StreamUtil::copy($this->handle); 825 | } 826 | 827 | /** 828 | * Returns the protocol from the internal URI. 829 | * 830 | * @return string The protocol. 831 | */ 832 | protected function getProtocol() 833 | { 834 | return substr($this->uri, 0, strpos($this->uri, '://')); 835 | } 836 | 837 | /** 838 | * Returns the local writable target of the resource within the stream. 839 | * 840 | * @param string|null $uri The URI. 841 | * 842 | * @return string The path appropriate for use with Flysystem. 843 | */ 844 | protected function getTarget($uri = null) 845 | { 846 | if ( ! isset($uri)) { 847 | $uri = $this->uri; 848 | } 849 | 850 | $target = substr($uri, strpos($uri, '://') + 3); 851 | 852 | return $target === false ? '' : $target; 853 | } 854 | 855 | /** 856 | * Returns the configuration. 857 | * 858 | * @param string|null $key The optional configuration key. 859 | * 860 | * @return array The requested configuration. 861 | */ 862 | protected function getConfiguration($key = null) 863 | { 864 | return $key ? static::$config[$this->getProtocol()][$key] : static::$config[$this->getProtocol()]; 865 | } 866 | 867 | /** 868 | * Returns the filesystem. 869 | * 870 | * @return \League\Flysystem\FilesystemInterface The filesystem object. 871 | */ 872 | protected function getFilesystem() 873 | { 874 | if (isset($this->filesystem)) { 875 | return $this->filesystem; 876 | } 877 | 878 | $this->filesystem = static::$filesystems[$this->getProtocol()]; 879 | 880 | return $this->filesystem; 881 | } 882 | 883 | /** 884 | * Calls a method on an object, catching any exceptions. 885 | * 886 | * @param object $object The object to call the method on. 887 | * @param string $method The method name. 888 | * @param array $args The arguments to the method. 889 | * @param string|null $errorname The name of the calling function. 890 | * 891 | * @return mixed|false The return value of the call, or false on failure. 892 | */ 893 | protected function invoke($object, $method, array $args, $errorname = null) 894 | { 895 | try { 896 | return call_user_func_array([$object, $method], $args); 897 | } catch (\Exception $e) { 898 | $errorname = $errorname ?: $method; 899 | $this->triggerError($errorname, $e); 900 | } 901 | 902 | return false; 903 | } 904 | 905 | /** 906 | * Calls trigger_error(), printing the appropriate message. 907 | * 908 | * @param string $function 909 | * @param \Exception $e 910 | */ 911 | protected function triggerError($function, \Exception $e) 912 | { 913 | if ($e instanceof TriggerErrorException) { 914 | trigger_error($e->formatMessage($function), E_USER_WARNING); 915 | 916 | return; 917 | } 918 | 919 | switch (get_class($e)) { 920 | case 'League\Flysystem\FileNotFoundException': 921 | trigger_error(sprintf('%s(): No such file or directory', $function), E_USER_WARNING); 922 | 923 | return; 924 | 925 | case 'League\Flysystem\RootViolationException': 926 | trigger_error(sprintf('%s(): Cannot remove the root directory', $function), E_USER_WARNING); 927 | 928 | return; 929 | } 930 | 931 | // Don't allow any exceptions to leak. 932 | trigger_error($e->getMessage(), E_USER_WARNING); 933 | } 934 | 935 | /** 936 | * Creates an advisory lock handle. 937 | * 938 | * @return resource|false 939 | */ 940 | protected function openLockHandle() 941 | { 942 | // PHP allows periods, '.', to be scheme names. Normalize the scheme 943 | // name to something that won't cause problems. Also, avoid problems 944 | // with case-insensitive filesystems. We use bin2hex() rather than a 945 | // hashing function since most scheme names are small, and bin2hex() 946 | // only doubles the string length. 947 | $sub_dir = bin2hex($this->getProtocol()); 948 | 949 | // Since we're flattening out whole filesystems, at least create a 950 | // sub-directory for each scheme to attempt to reduce the number of 951 | // files per directory. 952 | $temp_dir = sys_get_temp_dir() . '/flysystem-stream-wrapper/' . $sub_dir; 953 | 954 | // Race free directory creation. If @mkdir() fails, fopen() will fail 955 | // later, so there's no reason to test again. 956 | ! is_dir($temp_dir) && @mkdir($temp_dir, 0777, true); 957 | 958 | // Normalize paths so that locks are consistent. 959 | // We are using sha1() to avoid the file name limits, and case 960 | // insensitivity on Windows. This is not security sensitive. 961 | $lock_key = sha1(Util::normalizePath($this->getTarget())); 962 | 963 | // Relay the lock to a real filesystem lock. 964 | return fopen($temp_dir . '/' . $lock_key, 'c'); 965 | } 966 | 967 | /** 968 | * Releases the advisory lock. 969 | * 970 | * @param int $operation 971 | * 972 | * @return bool 973 | * 974 | * @see FlysystemStreamWrapper::stream_lock() 975 | */ 976 | protected function releaseLock($operation) 977 | { 978 | $exists = is_resource($this->lockHandle); 979 | 980 | $success = $exists && flock($this->lockHandle, $operation); 981 | 982 | $exists && fclose($this->lockHandle); 983 | $this->lockHandle = null; 984 | 985 | return $success; 986 | } 987 | } 988 | -------------------------------------------------------------------------------- /src/PosixUid.php: -------------------------------------------------------------------------------- 1 |