LocalFilesystemAdapter.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. declare(strict_types=1);
  3. namespace League\Flysystem\Local;
  4. use function file_put_contents;
  5. use const DIRECTORY_SEPARATOR;
  6. use const LOCK_EX;
  7. use DirectoryIterator;
  8. use FilesystemIterator;
  9. use Generator;
  10. use League\Flysystem\Config;
  11. use League\Flysystem\DirectoryAttributes;
  12. use League\Flysystem\FileAttributes;
  13. use League\Flysystem\FilesystemAdapter;
  14. use League\Flysystem\PathPrefixer;
  15. use League\Flysystem\SymbolicLinkEncountered;
  16. use League\Flysystem\UnableToCopyFile;
  17. use League\Flysystem\UnableToCreateDirectory;
  18. use League\Flysystem\UnableToDeleteDirectory;
  19. use League\Flysystem\UnableToDeleteFile;
  20. use League\Flysystem\UnableToMoveFile;
  21. use League\Flysystem\UnableToReadFile;
  22. use League\Flysystem\UnableToRetrieveMetadata;
  23. use League\Flysystem\UnableToSetVisibility;
  24. use League\Flysystem\UnableToWriteFile;
  25. use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
  26. use League\Flysystem\UnixVisibility\VisibilityConverter;
  27. use League\MimeTypeDetection\FinfoMimeTypeDetector;
  28. use League\MimeTypeDetection\MimeTypeDetector;
  29. use RecursiveDirectoryIterator;
  30. use RecursiveIteratorIterator;
  31. use SplFileInfo;
  32. use function chmod;
  33. use function clearstatcache;
  34. use function dirname;
  35. use function error_clear_last;
  36. use function error_get_last;
  37. use function file_exists;
  38. use function is_dir;
  39. use function is_file;
  40. use function mkdir;
  41. use function rename;
  42. use function stream_copy_to_stream;
  43. class LocalFilesystemAdapter implements FilesystemAdapter
  44. {
  45. /**
  46. * @var int
  47. */
  48. public const SKIP_LINKS = 0001;
  49. /**
  50. * @var int
  51. */
  52. public const DISALLOW_LINKS = 0002;
  53. /**
  54. * @var PathPrefixer
  55. */
  56. private $prefixer;
  57. /**
  58. * @var int
  59. */
  60. private $writeFlags;
  61. /**
  62. * @var int
  63. */
  64. private $linkHandling;
  65. /**
  66. * @var VisibilityConverter
  67. */
  68. private $visibility;
  69. /**
  70. * @var MimeTypeDetector
  71. */
  72. private $mimeTypeDetector;
  73. public function __construct(
  74. string $location,
  75. VisibilityConverter $visibility = null,
  76. int $writeFlags = LOCK_EX,
  77. int $linkHandling = self::DISALLOW_LINKS,
  78. MimeTypeDetector $mimeTypeDetector = null
  79. ) {
  80. $this->prefixer = new PathPrefixer($location, DIRECTORY_SEPARATOR);
  81. $this->writeFlags = $writeFlags;
  82. $this->linkHandling = $linkHandling;
  83. $this->visibility = $visibility ?: new PortableVisibilityConverter();
  84. $this->ensureDirectoryExists($location, $this->visibility->defaultForDirectories());
  85. $this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector();
  86. }
  87. public function write(string $path, string $contents, Config $config): void
  88. {
  89. $this->writeToFile($path, $contents, $config);
  90. }
  91. public function writeStream(string $path, $contents, Config $config): void
  92. {
  93. $this->writeToFile($path, $contents, $config);
  94. }
  95. /**
  96. * @param resource|string $contents
  97. */
  98. private function writeToFile(string $path, $contents, Config $config): void
  99. {
  100. $prefixedLocation = $this->prefixer->prefixPath($path);
  101. $this->ensureDirectoryExists(
  102. dirname($prefixedLocation),
  103. $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY))
  104. );
  105. error_clear_last();
  106. if (@file_put_contents($prefixedLocation, $contents, $this->writeFlags) === false) {
  107. throw UnableToWriteFile::atLocation($path, error_get_last()['message'] ?? '');
  108. }
  109. if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {
  110. $this->setVisibility($path, (string) $visibility);
  111. }
  112. }
  113. public function delete(string $path): void
  114. {
  115. $location = $this->prefixer->prefixPath($path);
  116. if ( ! file_exists($location)) {
  117. return;
  118. }
  119. error_clear_last();
  120. if ( ! @unlink($location)) {
  121. throw UnableToDeleteFile::atLocation($location, error_get_last()['message'] ?? '');
  122. }
  123. }
  124. public function deleteDirectory(string $prefix): void
  125. {
  126. $location = $this->prefixer->prefixPath($prefix);
  127. if ( ! is_dir($location)) {
  128. return;
  129. }
  130. $contents = $this->listDirectoryRecursively($location, RecursiveIteratorIterator::CHILD_FIRST);
  131. /** @var SplFileInfo $file */
  132. foreach ($contents as $file) {
  133. if ( ! $this->deleteFileInfoObject($file)) {
  134. throw UnableToDeleteDirectory::atLocation($prefix, "Unable to delete file at " . $file->getPathname());
  135. }
  136. }
  137. unset($contents);
  138. if ( ! @rmdir($location)) {
  139. throw UnableToDeleteDirectory::atLocation($prefix, error_get_last()['message'] ?? '');
  140. }
  141. }
  142. private function listDirectoryRecursively(
  143. string $path,
  144. int $mode = RecursiveIteratorIterator::SELF_FIRST
  145. ): Generator {
  146. yield from new RecursiveIteratorIterator(
  147. new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
  148. $mode
  149. );
  150. }
  151. protected function deleteFileInfoObject(SplFileInfo $file): bool
  152. {
  153. switch ($file->getType()) {
  154. case 'dir':
  155. return @rmdir((string) $file->getRealPath());
  156. case 'link':
  157. return @unlink((string) $file->getPathname());
  158. default:
  159. return @unlink((string) $file->getRealPath());
  160. }
  161. }
  162. public function listContents(string $path, bool $deep): iterable
  163. {
  164. $location = $this->prefixer->prefixPath($path);
  165. if ( ! is_dir($location)) {
  166. return;
  167. }
  168. /** @var SplFileInfo[] $iterator */
  169. $iterator = $deep ? $this->listDirectoryRecursively($location) : $this->listDirectory($location);
  170. foreach ($iterator as $fileInfo) {
  171. if ($fileInfo->isLink()) {
  172. if ($this->linkHandling & self::SKIP_LINKS) {
  173. continue;
  174. }
  175. throw SymbolicLinkEncountered::atLocation($fileInfo->getPathname());
  176. }
  177. $path = $this->prefixer->stripPrefix($fileInfo->getPathname());
  178. $lastModified = $fileInfo->getMTime();
  179. $isDirectory = $fileInfo->isDir();
  180. $permissions = octdec(substr(sprintf('%o', $fileInfo->getPerms()), -4));
  181. $visibility = $isDirectory ? $this->visibility->inverseForDirectory($permissions) : $this->visibility->inverseForFile($permissions);
  182. yield $isDirectory ? new DirectoryAttributes($path, $visibility, $lastModified) : new FileAttributes(
  183. str_replace('\\', '/', $path),
  184. $fileInfo->getSize(),
  185. $visibility,
  186. $lastModified
  187. );
  188. }
  189. }
  190. public function move(string $source, string $destination, Config $config): void
  191. {
  192. $sourcePath = $this->prefixer->prefixPath($source);
  193. $destinationPath = $this->prefixer->prefixPath($destination);
  194. $this->ensureDirectoryExists(
  195. dirname($destinationPath),
  196. $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY))
  197. );
  198. if ( ! @rename($sourcePath, $destinationPath)) {
  199. throw UnableToMoveFile::fromLocationTo($sourcePath, $destinationPath);
  200. }
  201. }
  202. public function copy(string $source, string $destination, Config $config): void
  203. {
  204. $sourcePath = $this->prefixer->prefixPath($source);
  205. $destinationPath = $this->prefixer->prefixPath($destination);
  206. $this->ensureDirectoryExists(
  207. dirname($destinationPath),
  208. $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY))
  209. );
  210. if ( ! @copy($sourcePath, $destinationPath)) {
  211. throw UnableToCopyFile::fromLocationTo($sourcePath, $destinationPath);
  212. }
  213. }
  214. public function read(string $path): string
  215. {
  216. $location = $this->prefixer->prefixPath($path);
  217. error_clear_last();
  218. $contents = @file_get_contents($location);
  219. if ($contents === false) {
  220. throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? '');
  221. }
  222. return $contents;
  223. }
  224. public function readStream(string $path)
  225. {
  226. $location = $this->prefixer->prefixPath($path);
  227. error_clear_last();
  228. $contents = @fopen($location, 'rb');
  229. if ($contents === false) {
  230. throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? '');
  231. }
  232. return $contents;
  233. }
  234. protected function ensureDirectoryExists(string $dirname, int $visibility): void
  235. {
  236. if (is_dir($dirname)) {
  237. return;
  238. }
  239. error_clear_last();
  240. if ( ! @mkdir($dirname, $visibility, true)) {
  241. $mkdirError = error_get_last();
  242. }
  243. clearstatcache(true, $dirname);
  244. if ( ! is_dir($dirname)) {
  245. $errorMessage = isset($mkdirError['message']) ? $mkdirError['message'] : '';
  246. throw UnableToCreateDirectory::atLocation($dirname, $errorMessage);
  247. }
  248. }
  249. public function fileExists(string $location): bool
  250. {
  251. $location = $this->prefixer->prefixPath($location);
  252. return is_file($location);
  253. }
  254. public function createDirectory(string $path, Config $config): void
  255. {
  256. $location = $this->prefixer->prefixPath($path);
  257. $visibility = $config->get(Config::OPTION_VISIBILITY, $config->get(Config::OPTION_DIRECTORY_VISIBILITY));
  258. $permissions = $this->resolveDirectoryVisibility($visibility);
  259. if (is_dir($location)) {
  260. $this->setPermissions($location, $permissions);
  261. return;
  262. }
  263. error_clear_last();
  264. if ( ! @mkdir($location, $permissions, true)) {
  265. throw UnableToCreateDirectory::atLocation($path, error_get_last()['message'] ?? '');
  266. }
  267. }
  268. public function setVisibility(string $path, string $visibility): void
  269. {
  270. $path = $this->prefixer->prefixPath($path);
  271. $visibility = is_dir($path) ? $this->visibility->forDirectory($visibility) : $this->visibility->forFile(
  272. $visibility
  273. );
  274. $this->setPermissions($path, $visibility);
  275. }
  276. public function visibility(string $path): FileAttributes
  277. {
  278. $location = $this->prefixer->prefixPath($path);
  279. clearstatcache(false, $location);
  280. error_clear_last();
  281. $fileperms = @fileperms($location);
  282. if ($fileperms === false) {
  283. throw UnableToRetrieveMetadata::visibility($path, error_get_last()['message'] ?? '');
  284. }
  285. $permissions = $fileperms & 0777;
  286. $visibility = $this->visibility->inverseForFile($permissions);
  287. return new FileAttributes($path, null, $visibility);
  288. }
  289. private function resolveDirectoryVisibility(?string $visibility): int
  290. {
  291. return $visibility === null ? $this->visibility->defaultForDirectories() : $this->visibility->forDirectory(
  292. $visibility
  293. );
  294. }
  295. public function mimeType(string $path): FileAttributes
  296. {
  297. $location = $this->prefixer->prefixPath($path);
  298. error_clear_last();
  299. $mimeType = $this->mimeTypeDetector->detectMimeTypeFromFile($location);
  300. if ($mimeType === null) {
  301. throw UnableToRetrieveMetadata::mimeType($path, error_get_last()['message'] ?? '');
  302. }
  303. return new FileAttributes($path, null, null, null, $mimeType);
  304. }
  305. public function lastModified(string $path): FileAttributes
  306. {
  307. $location = $this->prefixer->prefixPath($path);
  308. error_clear_last();
  309. $lastModified = @filemtime($location);
  310. if ($lastModified === false) {
  311. throw UnableToRetrieveMetadata::lastModified($path, error_get_last()['message'] ?? '');
  312. }
  313. return new FileAttributes($path, null, null, $lastModified);
  314. }
  315. public function fileSize(string $path): FileAttributes
  316. {
  317. $location = $this->prefixer->prefixPath($path);
  318. error_clear_last();
  319. if (is_file($location) && ($fileSize = @filesize($location)) !== false) {
  320. return new FileAttributes($path, $fileSize);
  321. }
  322. throw UnableToRetrieveMetadata::fileSize($path, error_get_last()['message'] ?? '');
  323. }
  324. private function listDirectory(string $location): Generator
  325. {
  326. $iterator = new DirectoryIterator($location);
  327. foreach ($iterator as $item) {
  328. if ($item->isDot()) {
  329. continue;
  330. }
  331. yield $item;
  332. }
  333. }
  334. private function setPermissions(string $location, int $visibility): void
  335. {
  336. error_clear_last();
  337. if ( ! @chmod($location, $visibility)) {
  338. $extraMessage = error_get_last()['message'] ?? '';
  339. throw UnableToSetVisibility::atLocation($this->prefixer->stripPrefix($location), $extraMessage);
  340. }
  341. }
  342. }