FilesUtil.php 13 KB

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