├── FallbackMimeTypeDetector.php ├── LICENSE ├── LocalFilesystemAdapter.php └── composer.json /FallbackMimeTypeDetector.php: -------------------------------------------------------------------------------- 1 | detector->detectMimeType($path, $contents); 30 | } 31 | 32 | public function detectMimeTypeFromBuffer(string $contents): ?string 33 | { 34 | return $this->detector->detectMimeTypeFromBuffer($contents); 35 | } 36 | 37 | public function detectMimeTypeFromPath(string $path): ?string 38 | { 39 | return $this->detector->detectMimeTypeFromPath($path); 40 | } 41 | 42 | public function detectMimeTypeFromFile(string $path): ?string 43 | { 44 | $mimeType = $this->detector->detectMimeTypeFromFile($path); 45 | 46 | if ($mimeType !== null && ! in_array($mimeType, $this->inconclusiveMimetypes)) { 47 | return $mimeType; 48 | } 49 | 50 | return $this->detector->detectMimeTypeFromPath($path) ?? ($this->useInconclusiveMimeTypeFallback ? $mimeType : null); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2024 Frank de Jonge 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LocalFilesystemAdapter.php: -------------------------------------------------------------------------------- 1 | prefixer = new PathPrefixer($location, DIRECTORY_SEPARATOR); 82 | $visibility ??= new PortableVisibilityConverter(); 83 | $this->visibility = $visibility; 84 | $this->rootLocation = $location; 85 | $this->mimeTypeDetector = $mimeTypeDetector ?? new FallbackMimeTypeDetector( 86 | detector: new FinfoMimeTypeDetector(), 87 | useInconclusiveMimeTypeFallback: $useInconclusiveMimeTypeFallback, 88 | ); 89 | 90 | if ( ! $lazyRootCreation) { 91 | $this->ensureRootDirectoryExists(); 92 | } 93 | } 94 | 95 | private function ensureRootDirectoryExists(): void 96 | { 97 | if ($this->rootLocationIsSetup) { 98 | return; 99 | } 100 | 101 | $this->ensureDirectoryExists($this->rootLocation, $this->visibility->defaultForDirectories()); 102 | $this->rootLocationIsSetup = true; 103 | } 104 | 105 | public function write(string $path, string $contents, Config $config): void 106 | { 107 | $this->writeToFile($path, $contents, $config); 108 | } 109 | 110 | public function writeStream(string $path, $contents, Config $config): void 111 | { 112 | $this->writeToFile($path, $contents, $config); 113 | } 114 | 115 | /** 116 | * @param resource|string $contents 117 | */ 118 | private function writeToFile(string $path, $contents, Config $config): void 119 | { 120 | $prefixedLocation = $this->prefixer->prefixPath($path); 121 | $this->ensureRootDirectoryExists(); 122 | $this->ensureDirectoryExists( 123 | dirname($prefixedLocation), 124 | $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) 125 | ); 126 | error_clear_last(); 127 | 128 | if (@file_put_contents($prefixedLocation, $contents, $this->writeFlags) === false) { 129 | throw UnableToWriteFile::atLocation($path, error_get_last()['message'] ?? ''); 130 | } 131 | 132 | if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { 133 | $this->setVisibility($path, (string) $visibility); 134 | } 135 | } 136 | 137 | public function delete(string $path): void 138 | { 139 | $location = $this->prefixer->prefixPath($path); 140 | 141 | if ( ! file_exists($location)) { 142 | return; 143 | } 144 | 145 | error_clear_last(); 146 | 147 | if ( ! @unlink($location)) { 148 | throw UnableToDeleteFile::atLocation($location, error_get_last()['message'] ?? ''); 149 | } 150 | } 151 | 152 | public function deleteDirectory(string $prefix): void 153 | { 154 | $location = $this->prefixer->prefixPath($prefix); 155 | 156 | if ( ! is_dir($location)) { 157 | return; 158 | } 159 | 160 | $contents = $this->listDirectoryRecursively($location, RecursiveIteratorIterator::CHILD_FIRST); 161 | 162 | /** @var SplFileInfo $file */ 163 | foreach ($contents as $file) { 164 | if ( ! $this->deleteFileInfoObject($file)) { 165 | throw UnableToDeleteDirectory::atLocation($prefix, "Unable to delete file at " . $file->getPathname()); 166 | } 167 | } 168 | 169 | unset($contents); 170 | 171 | if ( ! @rmdir($location)) { 172 | throw UnableToDeleteDirectory::atLocation($prefix, error_get_last()['message'] ?? ''); 173 | } 174 | } 175 | 176 | private function listDirectoryRecursively( 177 | string $path, 178 | int $mode = RecursiveIteratorIterator::SELF_FIRST 179 | ): Generator { 180 | if ( ! is_dir($path)) { 181 | return; 182 | } 183 | 184 | yield from new RecursiveIteratorIterator( 185 | new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), 186 | $mode 187 | ); 188 | } 189 | 190 | protected function deleteFileInfoObject(SplFileInfo $file): bool 191 | { 192 | switch ($file->getType()) { 193 | case 'dir': 194 | return @rmdir((string) $file->getRealPath()); 195 | case 'link': 196 | return @unlink((string) $file->getPathname()); 197 | default: 198 | return @unlink((string) $file->getRealPath()); 199 | } 200 | } 201 | 202 | public function listContents(string $path, bool $deep): iterable 203 | { 204 | $location = $this->prefixer->prefixPath($path); 205 | 206 | if ( ! is_dir($location)) { 207 | return; 208 | } 209 | 210 | /** @var SplFileInfo[] $iterator */ 211 | $iterator = $deep ? $this->listDirectoryRecursively($location) : $this->listDirectory($location); 212 | 213 | foreach ($iterator as $fileInfo) { 214 | $pathName = $fileInfo->getPathname(); 215 | 216 | try { 217 | if ($fileInfo->isLink()) { 218 | if ($this->linkHandling & self::SKIP_LINKS) { 219 | continue; 220 | } 221 | throw SymbolicLinkEncountered::atLocation($pathName); 222 | } 223 | 224 | $path = $this->prefixer->stripPrefix($pathName); 225 | $lastModified = $fileInfo->getMTime(); 226 | $isDirectory = $fileInfo->isDir(); 227 | $permissions = octdec(substr(sprintf('%o', $fileInfo->getPerms()), -4)); 228 | $visibility = $isDirectory ? $this->visibility->inverseForDirectory($permissions) : $this->visibility->inverseForFile($permissions); 229 | 230 | yield $isDirectory ? new DirectoryAttributes(str_replace('\\', '/', $path), $visibility, $lastModified) : new FileAttributes( 231 | str_replace('\\', '/', $path), 232 | $fileInfo->getSize(), 233 | $visibility, 234 | $lastModified 235 | ); 236 | } catch (Throwable $exception) { 237 | if (file_exists($pathName)) { 238 | throw $exception; 239 | } 240 | } 241 | } 242 | } 243 | 244 | public function move(string $source, string $destination, Config $config): void 245 | { 246 | $sourcePath = $this->prefixer->prefixPath($source); 247 | $destinationPath = $this->prefixer->prefixPath($destination); 248 | 249 | $this->ensureRootDirectoryExists(); 250 | $this->ensureDirectoryExists( 251 | dirname($destinationPath), 252 | $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) 253 | ); 254 | 255 | if ( ! @rename($sourcePath, $destinationPath)) { 256 | throw UnableToMoveFile::because(error_get_last()['message'] ?? 'unknown reason', $source, $destination); 257 | } 258 | 259 | if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { 260 | $this->setVisibility($destination, (string) $visibility); 261 | } 262 | } 263 | 264 | public function copy(string $source, string $destination, Config $config): void 265 | { 266 | $sourcePath = $this->prefixer->prefixPath($source); 267 | $destinationPath = $this->prefixer->prefixPath($destination); 268 | $this->ensureRootDirectoryExists(); 269 | $this->ensureDirectoryExists( 270 | dirname($destinationPath), 271 | $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) 272 | ); 273 | 274 | if ($sourcePath !== $destinationPath && ! @copy($sourcePath, $destinationPath)) { 275 | throw UnableToCopyFile::because(error_get_last()['message'] ?? 'unknown', $source, $destination); 276 | } 277 | 278 | $visibility = $config->get( 279 | Config::OPTION_VISIBILITY, 280 | $config->get(Config::OPTION_RETAIN_VISIBILITY, true) 281 | ? $this->visibility($source)->visibility() 282 | : null, 283 | ); 284 | 285 | if ($visibility) { 286 | $this->setVisibility($destination, (string) $visibility); 287 | } 288 | } 289 | 290 | public function read(string $path): string 291 | { 292 | $location = $this->prefixer->prefixPath($path); 293 | error_clear_last(); 294 | $contents = @file_get_contents($location); 295 | 296 | if ($contents === false) { 297 | throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? ''); 298 | } 299 | 300 | return $contents; 301 | } 302 | 303 | public function readStream(string $path) 304 | { 305 | $location = $this->prefixer->prefixPath($path); 306 | error_clear_last(); 307 | $contents = @fopen($location, 'rb'); 308 | 309 | if ($contents === false) { 310 | throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? ''); 311 | } 312 | 313 | return $contents; 314 | } 315 | 316 | protected function ensureDirectoryExists(string $dirname, int $visibility): void 317 | { 318 | if (is_dir($dirname)) { 319 | return; 320 | } 321 | 322 | error_clear_last(); 323 | 324 | if ( ! @mkdir($dirname, $visibility, true)) { 325 | $mkdirError = error_get_last(); 326 | } 327 | 328 | clearstatcache(true, $dirname); 329 | 330 | if ( ! is_dir($dirname)) { 331 | $errorMessage = isset($mkdirError['message']) ? $mkdirError['message'] : ''; 332 | 333 | throw UnableToCreateDirectory::atLocation($dirname, $errorMessage); 334 | } 335 | } 336 | 337 | public function fileExists(string $location): bool 338 | { 339 | $location = $this->prefixer->prefixPath($location); 340 | 341 | return is_file($location); 342 | } 343 | 344 | public function directoryExists(string $location): bool 345 | { 346 | $location = $this->prefixer->prefixPath($location); 347 | 348 | return is_dir($location); 349 | } 350 | 351 | public function createDirectory(string $path, Config $config): void 352 | { 353 | $this->ensureRootDirectoryExists(); 354 | $location = $this->prefixer->prefixPath($path); 355 | $visibility = $config->get(Config::OPTION_VISIBILITY, $config->get(Config::OPTION_DIRECTORY_VISIBILITY)); 356 | $permissions = $this->resolveDirectoryVisibility($visibility); 357 | 358 | if (is_dir($location)) { 359 | $this->setPermissions($location, $permissions); 360 | 361 | return; 362 | } 363 | 364 | error_clear_last(); 365 | 366 | if ( ! @mkdir($location, $permissions, true)) { 367 | throw UnableToCreateDirectory::atLocation($path, error_get_last()['message'] ?? ''); 368 | } 369 | } 370 | 371 | public function setVisibility(string $path, string $visibility): void 372 | { 373 | $path = $this->prefixer->prefixPath($path); 374 | $visibility = is_dir($path) ? $this->visibility->forDirectory($visibility) : $this->visibility->forFile( 375 | $visibility 376 | ); 377 | 378 | $this->setPermissions($path, $visibility); 379 | } 380 | 381 | public function visibility(string $path): FileAttributes 382 | { 383 | $location = $this->prefixer->prefixPath($path); 384 | clearstatcache(false, $location); 385 | error_clear_last(); 386 | $fileperms = @fileperms($location); 387 | 388 | if ($fileperms === false) { 389 | throw UnableToRetrieveMetadata::visibility($path, error_get_last()['message'] ?? ''); 390 | } 391 | 392 | $permissions = $fileperms & 0777; 393 | $visibility = $this->visibility->inverseForFile($permissions); 394 | 395 | return new FileAttributes($path, null, $visibility); 396 | } 397 | 398 | private function resolveDirectoryVisibility(?string $visibility): int 399 | { 400 | return $visibility === null ? $this->visibility->defaultForDirectories() : $this->visibility->forDirectory( 401 | $visibility 402 | ); 403 | } 404 | 405 | public function mimeType(string $path): FileAttributes 406 | { 407 | $location = $this->prefixer->prefixPath($path); 408 | error_clear_last(); 409 | 410 | if ( ! is_file($location)) { 411 | throw UnableToRetrieveMetadata::mimeType($location, 'No such file exists.'); 412 | } 413 | 414 | $mimeType = $this->mimeTypeDetector->detectMimeTypeFromFile($location); 415 | 416 | if ($mimeType === null) { 417 | throw UnableToRetrieveMetadata::mimeType($path, error_get_last()['message'] ?? ''); 418 | } 419 | 420 | return new FileAttributes($path, null, null, null, $mimeType); 421 | } 422 | 423 | public function lastModified(string $path): FileAttributes 424 | { 425 | $location = $this->prefixer->prefixPath($path); 426 | error_clear_last(); 427 | $lastModified = @filemtime($location); 428 | 429 | if ($lastModified === false) { 430 | throw UnableToRetrieveMetadata::lastModified($path, error_get_last()['message'] ?? ''); 431 | } 432 | 433 | return new FileAttributes($path, null, null, $lastModified); 434 | } 435 | 436 | public function fileSize(string $path): FileAttributes 437 | { 438 | $location = $this->prefixer->prefixPath($path); 439 | error_clear_last(); 440 | 441 | if (is_file($location) && ($fileSize = @filesize($location)) !== false) { 442 | return new FileAttributes($path, $fileSize); 443 | } 444 | 445 | throw UnableToRetrieveMetadata::fileSize($path, error_get_last()['message'] ?? ''); 446 | } 447 | 448 | public function checksum(string $path, Config $config): string 449 | { 450 | $algo = $config->get('checksum_algo', 'md5'); 451 | $location = $this->prefixer->prefixPath($path); 452 | error_clear_last(); 453 | $checksum = @hash_file($algo, $location); 454 | 455 | if ($checksum === false) { 456 | throw new UnableToProvideChecksum(error_get_last()['message'] ?? '', $path); 457 | } 458 | 459 | return $checksum; 460 | } 461 | 462 | private function listDirectory(string $location): Generator 463 | { 464 | $iterator = new DirectoryIterator($location); 465 | 466 | foreach ($iterator as $item) { 467 | if ($item->isDot()) { 468 | continue; 469 | } 470 | 471 | yield $item; 472 | } 473 | } 474 | 475 | private function setPermissions(string $location, int $visibility): void 476 | { 477 | error_clear_last(); 478 | if ( ! @chmod($location, $visibility)) { 479 | $extraMessage = error_get_last()['message'] ?? ''; 480 | throw UnableToSetVisibility::atLocation($this->prefixer->stripPrefix($location), $extraMessage); 481 | } 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/flysystem-local", 3 | "description": "Local filesystem adapter for Flysystem.", 4 | "keywords": ["flysystem", "filesystem", "local", "file", "files"], 5 | "type": "library", 6 | "prefer-stable": true, 7 | "autoload": { 8 | "psr-4": { 9 | "League\\Flysystem\\Local\\": "" 10 | } 11 | }, 12 | "require": { 13 | "php": "^8.0.2", 14 | "ext-fileinfo": "*", 15 | "league/flysystem": "^3.0.0", 16 | "league/mime-type-detection": "^1.0.0" 17 | }, 18 | "license": "MIT", 19 | "authors": [ 20 | { 21 | "name": "Frank de Jonge", 22 | "email": "info@frankdejonge.nl" 23 | } 24 | ] 25 | } 26 | --------------------------------------------------------------------------------