1<?php 2 3namespace League\Flysystem\Adapter; 4 5use DirectoryIterator; 6use FilesystemIterator; 7use finfo as Finfo; 8use League\Flysystem\Config; 9use League\Flysystem\Exception; 10use League\Flysystem\NotSupportedException; 11use League\Flysystem\UnreadableFileException; 12use League\Flysystem\Util; 13use LogicException; 14use RecursiveDirectoryIterator; 15use RecursiveIteratorIterator; 16use SplFileInfo; 17 18class Local extends AbstractAdapter 19{ 20 /** 21 * @var int 22 */ 23 const SKIP_LINKS = 0001; 24 25 /** 26 * @var int 27 */ 28 const DISALLOW_LINKS = 0002; 29 30 /** 31 * @var array 32 */ 33 protected static $permissions = [ 34 'file' => [ 35 'public' => 0644, 36 'private' => 0600, 37 ], 38 'dir' => [ 39 'public' => 0755, 40 'private' => 0700, 41 ], 42 ]; 43 44 /** 45 * @var string 46 */ 47 protected $pathSeparator = DIRECTORY_SEPARATOR; 48 49 /** 50 * @var array 51 */ 52 protected $permissionMap; 53 54 /** 55 * @var int 56 */ 57 protected $writeFlags; 58 59 /** 60 * @var int 61 */ 62 private $linkHandling; 63 64 /** 65 * Constructor. 66 * 67 * @param string $root 68 * @param int $writeFlags 69 * @param int $linkHandling 70 * @param array $permissions 71 * 72 * @throws LogicException 73 */ 74 public function __construct($root, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS, array $permissions = []) 75 { 76 $root = is_link($root) ? realpath($root) : $root; 77 $this->permissionMap = array_replace_recursive(static::$permissions, $permissions); 78 $this->ensureDirectory($root); 79 80 if ( ! is_dir($root) || ! is_readable($root)) { 81 throw new LogicException('The root path ' . $root . ' is not readable.'); 82 } 83 84 $this->setPathPrefix($root); 85 $this->writeFlags = $writeFlags; 86 $this->linkHandling = $linkHandling; 87 } 88 89 /** 90 * Ensure the root directory exists. 91 * 92 * @param string $root root directory path 93 * 94 * @return void 95 * 96 * @throws Exception in case the root directory can not be created 97 */ 98 protected function ensureDirectory($root) 99 { 100 if ( ! is_dir($root)) { 101 $umask = umask(0); 102 103 if ( ! @mkdir($root, $this->permissionMap['dir']['public'], true)) { 104 $mkdirError = error_get_last(); 105 } 106 107 umask($umask); 108 clearstatcache(false, $root); 109 110 if ( ! is_dir($root)) { 111 $errorMessage = isset($mkdirError['message']) ? $mkdirError['message'] : ''; 112 throw new Exception(sprintf('Impossible to create the root directory "%s". %s', $root, $errorMessage)); 113 } 114 } 115 } 116 117 /** 118 * @inheritdoc 119 */ 120 public function has($path) 121 { 122 $location = $this->applyPathPrefix($path); 123 124 return file_exists($location); 125 } 126 127 /** 128 * @inheritdoc 129 */ 130 public function write($path, $contents, Config $config) 131 { 132 $location = $this->applyPathPrefix($path); 133 $this->ensureDirectory(dirname($location)); 134 135 if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) { 136 return false; 137 } 138 139 $type = 'file'; 140 $result = compact('contents', 'type', 'size', 'path'); 141 142 if ($visibility = $config->get('visibility')) { 143 $result['visibility'] = $visibility; 144 $this->setVisibility($path, $visibility); 145 } 146 147 return $result; 148 } 149 150 /** 151 * @inheritdoc 152 */ 153 public function writeStream($path, $resource, Config $config) 154 { 155 $location = $this->applyPathPrefix($path); 156 $this->ensureDirectory(dirname($location)); 157 $stream = fopen($location, 'w+b'); 158 159 if ( ! $stream || stream_copy_to_stream($resource, $stream) === false || ! fclose($stream)) { 160 return false; 161 } 162 163 $type = 'file'; 164 $result = compact('type', 'path'); 165 166 if ($visibility = $config->get('visibility')) { 167 $this->setVisibility($path, $visibility); 168 $result['visibility'] = $visibility; 169 } 170 171 return $result; 172 } 173 174 /** 175 * @inheritdoc 176 */ 177 public function readStream($path) 178 { 179 $location = $this->applyPathPrefix($path); 180 $stream = fopen($location, 'rb'); 181 182 return ['type' => 'file', 'path' => $path, 'stream' => $stream]; 183 } 184 185 /** 186 * @inheritdoc 187 */ 188 public function updateStream($path, $resource, Config $config) 189 { 190 return $this->writeStream($path, $resource, $config); 191 } 192 193 /** 194 * @inheritdoc 195 */ 196 public function update($path, $contents, Config $config) 197 { 198 $location = $this->applyPathPrefix($path); 199 $size = file_put_contents($location, $contents, $this->writeFlags); 200 201 if ($size === false) { 202 return false; 203 } 204 205 $type = 'file'; 206 207 $result = compact('type', 'path', 'size', 'contents'); 208 209 if ($visibility = $config->get('visibility')) { 210 $this->setVisibility($path, $visibility); 211 $result['visibility'] = $visibility; 212 } 213 214 return $result; 215 } 216 217 /** 218 * @inheritdoc 219 */ 220 public function read($path) 221 { 222 $location = $this->applyPathPrefix($path); 223 $contents = @file_get_contents($location); 224 225 if ($contents === false) { 226 return false; 227 } 228 229 return ['type' => 'file', 'path' => $path, 'contents' => $contents]; 230 } 231 232 /** 233 * @inheritdoc 234 */ 235 public function rename($path, $newpath) 236 { 237 $location = $this->applyPathPrefix($path); 238 $destination = $this->applyPathPrefix($newpath); 239 $parentDirectory = $this->applyPathPrefix(Util::dirname($newpath)); 240 $this->ensureDirectory($parentDirectory); 241 242 return rename($location, $destination); 243 } 244 245 /** 246 * @inheritdoc 247 */ 248 public function copy($path, $newpath) 249 { 250 $location = $this->applyPathPrefix($path); 251 $destination = $this->applyPathPrefix($newpath); 252 $this->ensureDirectory(dirname($destination)); 253 254 return copy($location, $destination); 255 } 256 257 /** 258 * @inheritdoc 259 */ 260 public function delete($path) 261 { 262 $location = $this->applyPathPrefix($path); 263 264 return @unlink($location); 265 } 266 267 /** 268 * @inheritdoc 269 */ 270 public function listContents($directory = '', $recursive = false) 271 { 272 $result = []; 273 $location = $this->applyPathPrefix($directory); 274 275 if ( ! is_dir($location)) { 276 return []; 277 } 278 279 $iterator = $recursive ? $this->getRecursiveDirectoryIterator($location) : $this->getDirectoryIterator($location); 280 281 foreach ($iterator as $file) { 282 $path = $this->getFilePath($file); 283 284 if (preg_match('#(^|/|\\\\)\.{1,2}$#', $path)) { 285 continue; 286 } 287 288 $result[] = $this->normalizeFileInfo($file); 289 } 290 291 unset($iterator); 292 293 return array_filter($result); 294 } 295 296 /** 297 * @inheritdoc 298 */ 299 public function getMetadata($path) 300 { 301 $location = $this->applyPathPrefix($path); 302 clearstatcache(false, $location); 303 $info = new SplFileInfo($location); 304 305 return $this->normalizeFileInfo($info); 306 } 307 308 /** 309 * @inheritdoc 310 */ 311 public function getSize($path) 312 { 313 return $this->getMetadata($path); 314 } 315 316 /** 317 * @inheritdoc 318 */ 319 public function getMimetype($path) 320 { 321 $location = $this->applyPathPrefix($path); 322 $finfo = new Finfo(FILEINFO_MIME_TYPE); 323 $mimetype = $finfo->file($location); 324 325 if (in_array($mimetype, ['application/octet-stream', 'inode/x-empty', 'application/x-empty'])) { 326 $mimetype = Util\MimeType::detectByFilename($location); 327 } 328 329 return ['path' => $path, 'type' => 'file', 'mimetype' => $mimetype]; 330 } 331 332 /** 333 * @inheritdoc 334 */ 335 public function getTimestamp($path) 336 { 337 return $this->getMetadata($path); 338 } 339 340 /** 341 * @inheritdoc 342 */ 343 public function getVisibility($path) 344 { 345 $location = $this->applyPathPrefix($path); 346 clearstatcache(false, $location); 347 $permissions = octdec(substr(sprintf('%o', fileperms($location)), -4)); 348 $type = is_dir($location) ? 'dir' : 'file'; 349 350 foreach ($this->permissionMap[$type] as $visibility => $visibilityPermissions) { 351 if ($visibilityPermissions == $permissions) { 352 return compact('path', 'visibility'); 353 } 354 } 355 356 $visibility = substr(sprintf('%o', fileperms($location)), -4); 357 358 return compact('path', 'visibility'); 359 } 360 361 /** 362 * @inheritdoc 363 */ 364 public function setVisibility($path, $visibility) 365 { 366 $location = $this->applyPathPrefix($path); 367 $type = is_dir($location) ? 'dir' : 'file'; 368 $success = chmod($location, $this->permissionMap[$type][$visibility]); 369 370 if ($success === false) { 371 return false; 372 } 373 374 return compact('path', 'visibility'); 375 } 376 377 /** 378 * @inheritdoc 379 */ 380 public function createDir($dirname, Config $config) 381 { 382 $location = $this->applyPathPrefix($dirname); 383 $umask = umask(0); 384 $visibility = $config->get('visibility', 'public'); 385 $return = ['path' => $dirname, 'type' => 'dir']; 386 387 if ( ! is_dir($location)) { 388 if (false === @mkdir($location, $this->permissionMap['dir'][$visibility], true) 389 || false === is_dir($location)) { 390 $return = false; 391 } 392 } 393 394 umask($umask); 395 396 return $return; 397 } 398 399 /** 400 * @inheritdoc 401 */ 402 public function deleteDir($dirname) 403 { 404 $location = $this->applyPathPrefix($dirname); 405 406 if ( ! is_dir($location)) { 407 return false; 408 } 409 410 $contents = $this->getRecursiveDirectoryIterator($location, RecursiveIteratorIterator::CHILD_FIRST); 411 412 /** @var SplFileInfo $file */ 413 foreach ($contents as $file) { 414 $this->guardAgainstUnreadableFileInfo($file); 415 $this->deleteFileInfoObject($file); 416 } 417 418 unset($contents); 419 420 return rmdir($location); 421 } 422 423 /** 424 * @param SplFileInfo $file 425 */ 426 protected function deleteFileInfoObject(SplFileInfo $file) 427 { 428 switch ($file->getType()) { 429 case 'dir': 430 rmdir($file->getRealPath()); 431 break; 432 case 'link': 433 unlink($file->getPathname()); 434 break; 435 default: 436 unlink($file->getRealPath()); 437 } 438 } 439 440 /** 441 * Normalize the file info. 442 * 443 * @param SplFileInfo $file 444 * 445 * @return array|void 446 * 447 * @throws NotSupportedException 448 */ 449 protected function normalizeFileInfo(SplFileInfo $file) 450 { 451 if ( ! $file->isLink()) { 452 return $this->mapFileInfo($file); 453 } 454 455 if ($this->linkHandling & self::DISALLOW_LINKS) { 456 throw NotSupportedException::forLink($file); 457 } 458 } 459 460 /** 461 * Get the normalized path from a SplFileInfo object. 462 * 463 * @param SplFileInfo $file 464 * 465 * @return string 466 */ 467 protected function getFilePath(SplFileInfo $file) 468 { 469 $location = $file->getPathname(); 470 $path = $this->removePathPrefix($location); 471 472 return trim(str_replace('\\', '/', $path), '/'); 473 } 474 475 /** 476 * @param string $path 477 * @param int $mode 478 * 479 * @return RecursiveIteratorIterator 480 */ 481 protected function getRecursiveDirectoryIterator($path, $mode = RecursiveIteratorIterator::SELF_FIRST) 482 { 483 return new RecursiveIteratorIterator( 484 new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), 485 $mode 486 ); 487 } 488 489 /** 490 * @param string $path 491 * 492 * @return DirectoryIterator 493 */ 494 protected function getDirectoryIterator($path) 495 { 496 $iterator = new DirectoryIterator($path); 497 498 return $iterator; 499 } 500 501 /** 502 * @param SplFileInfo $file 503 * 504 * @return array 505 */ 506 protected function mapFileInfo(SplFileInfo $file) 507 { 508 $normalized = [ 509 'type' => $file->getType(), 510 'path' => $this->getFilePath($file), 511 ]; 512 513 $normalized['timestamp'] = $file->getMTime(); 514 515 if ($normalized['type'] === 'file') { 516 $normalized['size'] = $file->getSize(); 517 } 518 519 return $normalized; 520 } 521 522 /** 523 * @param SplFileInfo $file 524 * 525 * @throws UnreadableFileException 526 */ 527 protected function guardAgainstUnreadableFileInfo(SplFileInfo $file) 528 { 529 if ( ! $file->isReadable()) { 530 throw UnreadableFileException::forFileInfo($file); 531 } 532 } 533} 534