Local.php 13 KB

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