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