FilesUtil.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of the nelexa/zip package.
  5. * (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. namespace PhpZip\Util;
  10. use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
  11. use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
  12. /**
  13. * Files util.
  14. *
  15. * @internal
  16. */
  17. final class FilesUtil
  18. {
  19. /**
  20. * Is empty directory.
  21. *
  22. * @param string $dir Directory
  23. */
  24. public static function isEmptyDir(string $dir): bool
  25. {
  26. if (!is_readable($dir)) {
  27. return false;
  28. }
  29. return \count(scandir($dir)) === 2;
  30. }
  31. /**
  32. * Remove recursive directory.
  33. *
  34. * @param string $dir directory path
  35. */
  36. public static function removeDir(string $dir): void
  37. {
  38. $files = new \RecursiveIteratorIterator(
  39. new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
  40. \RecursiveIteratorIterator::CHILD_FIRST
  41. );
  42. /** @var \SplFileInfo $fileInfo */
  43. foreach ($files as $fileInfo) {
  44. $function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
  45. $function($fileInfo->getPathname());
  46. }
  47. @rmdir($dir);
  48. }
  49. /**
  50. * Convert glob pattern to regex pattern.
  51. */
  52. public static function convertGlobToRegEx(string $globPattern): string
  53. {
  54. // Remove beginning and ending * globs because they're useless
  55. $globPattern = trim($globPattern, '*');
  56. $escaping = false;
  57. $inCurrent = 0;
  58. $chars = str_split($globPattern);
  59. $regexPattern = '';
  60. foreach ($chars as $currentChar) {
  61. switch ($currentChar) {
  62. case '*':
  63. $regexPattern .= ($escaping ? '\\*' : '.*');
  64. $escaping = false;
  65. break;
  66. case '?':
  67. $regexPattern .= ($escaping ? '\\?' : '.');
  68. $escaping = false;
  69. break;
  70. case '.':
  71. case '(':
  72. case ')':
  73. case '+':
  74. case '|':
  75. case '^':
  76. case '$':
  77. case '@':
  78. case '%':
  79. $regexPattern .= '\\' . $currentChar;
  80. $escaping = false;
  81. break;
  82. case '\\':
  83. if ($escaping) {
  84. $regexPattern .= '\\\\';
  85. $escaping = false;
  86. } else {
  87. $escaping = true;
  88. }
  89. break;
  90. case '{':
  91. if ($escaping) {
  92. $regexPattern .= '\\{';
  93. } else {
  94. $regexPattern = '(';
  95. $inCurrent++;
  96. }
  97. $escaping = false;
  98. break;
  99. case '}':
  100. if ($inCurrent > 0 && !$escaping) {
  101. $regexPattern .= ')';
  102. $inCurrent--;
  103. } elseif ($escaping) {
  104. $regexPattern = '\\}';
  105. } else {
  106. $regexPattern = '}';
  107. }
  108. $escaping = false;
  109. break;
  110. case ',':
  111. if ($inCurrent > 0 && !$escaping) {
  112. $regexPattern .= '|';
  113. } elseif ($escaping) {
  114. $regexPattern .= '\\,';
  115. } else {
  116. $regexPattern = ',';
  117. }
  118. break;
  119. default:
  120. $escaping = false;
  121. $regexPattern .= $currentChar;
  122. }
  123. }
  124. return $regexPattern;
  125. }
  126. /**
  127. * Search files.
  128. *
  129. * @return array Searched file list
  130. */
  131. public static function fileSearchWithIgnore(string $inputDir, bool $recursive = true, array $ignoreFiles = []): array
  132. {
  133. if ($recursive) {
  134. $directoryIterator = new \RecursiveDirectoryIterator($inputDir);
  135. if (!empty($ignoreFiles)) {
  136. $directoryIterator = new IgnoreFilesRecursiveFilterIterator($directoryIterator, $ignoreFiles);
  137. }
  138. $iterator = new \RecursiveIteratorIterator($directoryIterator);
  139. } else {
  140. $directoryIterator = new \DirectoryIterator($inputDir);
  141. if (!empty($ignoreFiles)) {
  142. $directoryIterator = new IgnoreFilesFilterIterator($directoryIterator, $ignoreFiles);
  143. }
  144. $iterator = new \IteratorIterator($directoryIterator);
  145. }
  146. $fileList = [];
  147. foreach ($iterator as $file) {
  148. if ($file instanceof \SplFileInfo) {
  149. $fileList[] = $file->getPathname();
  150. }
  151. }
  152. return $fileList;
  153. }
  154. /**
  155. * Search files from glob pattern.
  156. *
  157. * @return array Searched file list
  158. */
  159. public static function globFileSearch(string $globPattern, int $flags = 0, bool $recursive = true): array
  160. {
  161. $files = glob($globPattern, $flags);
  162. if (!$recursive) {
  163. return $files;
  164. }
  165. foreach (glob(\dirname($globPattern) . \DIRECTORY_SEPARATOR . '*', \GLOB_ONLYDIR | \GLOB_NOSORT) as $dir) {
  166. // Unpacking the argument via ... is supported starting from php 5.6 only
  167. /** @noinspection SlowArrayOperationsInLoopInspection */
  168. $files = array_merge($files, self::globFileSearch($dir . \DIRECTORY_SEPARATOR . basename($globPattern), $flags, $recursive));
  169. }
  170. return $files;
  171. }
  172. /**
  173. * Search files from regex pattern.
  174. *
  175. * @return array Searched file list
  176. */
  177. public static function regexFileSearch(string $folder, string $pattern, bool $recursive = true): array
  178. {
  179. if ($recursive) {
  180. $directoryIterator = new \RecursiveDirectoryIterator($folder);
  181. $iterator = new \RecursiveIteratorIterator($directoryIterator);
  182. } else {
  183. $directoryIterator = new \DirectoryIterator($folder);
  184. $iterator = new \IteratorIterator($directoryIterator);
  185. }
  186. $regexIterator = new \RegexIterator($iterator, $pattern, \RegexIterator::MATCH);
  187. $fileList = [];
  188. foreach ($regexIterator as $file) {
  189. if ($file instanceof \SplFileInfo) {
  190. $fileList[] = $file->getPathname();
  191. }
  192. }
  193. return $fileList;
  194. }
  195. /**
  196. * Convert bytes to human size.
  197. *
  198. * @param int $size Size bytes
  199. * @param string|null $unit Unit support 'GB', 'MB', 'KB'
  200. */
  201. public static function humanSize(int $size, ?string $unit = null): string
  202. {
  203. if (($unit === null && $size >= 1 << 30) || $unit === 'GB') {
  204. return number_format($size / (1 << 30), 2) . 'GB';
  205. }
  206. if (($unit === null && $size >= 1 << 20) || $unit === 'MB') {
  207. return number_format($size / (1 << 20), 2) . 'MB';
  208. }
  209. if (($unit === null && $size >= 1 << 10) || $unit === 'KB') {
  210. return number_format($size / (1 << 10), 2) . 'KB';
  211. }
  212. return number_format($size) . ' bytes';
  213. }
  214. /**
  215. * Normalizes zip path.
  216. *
  217. * @param string $path Zip path
  218. */
  219. public static function normalizeZipPath(string $path): string
  220. {
  221. return implode(
  222. \DIRECTORY_SEPARATOR,
  223. array_filter(
  224. explode('/', $path),
  225. static fn ($part) => $part !== '.' && $part !== '..'
  226. )
  227. );
  228. }
  229. /**
  230. * Returns whether the file path is an absolute path.
  231. *
  232. * @param string $file A file path
  233. *
  234. * @see source symfony filesystem component
  235. */
  236. public static function isAbsolutePath(string $file): bool
  237. {
  238. return strspn($file, '/\\', 0, 1)
  239. || (
  240. \strlen($file) > 3 && ctype_alpha($file[0])
  241. && $file[1] === ':'
  242. && strspn($file, '/\\', 2, 1)
  243. )
  244. || parse_url($file, \PHP_URL_SCHEME) !== null;
  245. }
  246. public static function symlink(string $target, string $path, bool $allowSymlink): bool
  247. {
  248. if (\DIRECTORY_SEPARATOR === '\\' || !$allowSymlink) {
  249. return file_put_contents($path, $target) !== false;
  250. }
  251. return symlink($target, $path);
  252. }
  253. public static function isBadCompressionFile(string $file): bool
  254. {
  255. $badCompressFileExt = [
  256. 'dic',
  257. 'dng',
  258. 'f4v',
  259. 'flipchart',
  260. 'h264',
  261. 'lrf',
  262. 'mobi',
  263. 'mts',
  264. 'nef',
  265. 'pspimage',
  266. ];
  267. $ext = strtolower(pathinfo($file, \PATHINFO_EXTENSION));
  268. if (\in_array($ext, $badCompressFileExt, true)) {
  269. return true;
  270. }
  271. $mimeType = self::getMimeTypeFromFile($file);
  272. return self::isBadCompressionMimeType($mimeType);
  273. }
  274. public static function isBadCompressionMimeType(string $mimeType): bool
  275. {
  276. static $badDeflateCompMimeTypes = [
  277. 'application/epub+zip',
  278. 'application/gzip',
  279. 'application/vnd.debian.binary-package',
  280. 'application/vnd.oasis.opendocument.graphics',
  281. 'application/vnd.oasis.opendocument.presentation',
  282. 'application/vnd.oasis.opendocument.text',
  283. 'application/vnd.oasis.opendocument.text-master',
  284. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  285. 'application/vnd.rn-realmedia',
  286. 'application/x-7z-compressed',
  287. 'application/x-arj',
  288. 'application/x-bzip2',
  289. 'application/x-hwp',
  290. 'application/x-lzip',
  291. 'application/x-lzma',
  292. 'application/x-ms-reader',
  293. 'application/x-rar',
  294. 'application/x-rpm',
  295. 'application/x-stuffit',
  296. 'application/x-tar',
  297. 'application/x-xz',
  298. 'application/zip',
  299. 'application/zlib',
  300. 'audio/flac',
  301. 'audio/mpeg',
  302. 'audio/ogg',
  303. 'audio/vnd.dolby.dd-raw',
  304. 'audio/webm',
  305. 'audio/x-ape',
  306. 'audio/x-hx-aac-adts',
  307. 'audio/x-m4a',
  308. 'audio/x-m4a',
  309. 'audio/x-wav',
  310. 'image/gif',
  311. 'image/heic',
  312. 'image/jp2',
  313. 'image/jpeg',
  314. 'image/png',
  315. 'image/vnd.djvu',
  316. 'image/webp',
  317. 'image/x-canon-cr2',
  318. 'video/ogg',
  319. 'video/webm',
  320. 'video/x-matroska',
  321. 'video/x-ms-asf',
  322. 'x-epoc/x-sisx-app',
  323. ];
  324. return \in_array($mimeType, $badDeflateCompMimeTypes, true);
  325. }
  326. /**
  327. * @noinspection PhpComposerExtensionStubsInspection
  328. */
  329. public static function getMimeTypeFromFile(string $file): string
  330. {
  331. if (\function_exists('mime_content_type')) {
  332. return mime_content_type($file);
  333. }
  334. return 'application/octet-stream';
  335. }
  336. /**
  337. * @noinspection PhpComposerExtensionStubsInspection
  338. */
  339. public static function getMimeTypeFromString(string $contents): string
  340. {
  341. $finfo = new \finfo(\FILEINFO_MIME);
  342. $mimeType = $finfo->buffer($contents);
  343. if ($mimeType === false) {
  344. $mimeType = 'application/octet-stream';
  345. }
  346. return explode(';', $mimeType)[0];
  347. }
  348. }