Tar.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Archive;
  7. use Magento\Framework\Archive\Helper\File;
  8. /**
  9. * Class to work with tar archives
  10. *
  11. * @author Magento Core Team <core@magentocommerce.com>
  12. */
  13. class Tar extends \Magento\Framework\Archive\AbstractArchive implements \Magento\Framework\Archive\ArchiveInterface
  14. {
  15. /**
  16. * Tar block size
  17. *
  18. * @const int
  19. */
  20. const TAR_BLOCK_SIZE = 512;
  21. /**
  22. * Keep file or directory for packing.
  23. *
  24. * @var string
  25. */
  26. protected $_currentFile;
  27. /**
  28. * Keep path to file or directory for packing.
  29. *
  30. * @var string
  31. */
  32. protected $_currentPath;
  33. /**
  34. * Skip first level parent directory. Example:
  35. * use test/fip.php instead test/test/fip.php;
  36. *
  37. * @var bool
  38. */
  39. protected $_skipRoot;
  40. /**
  41. * Tarball data writer
  42. *
  43. * @var File
  44. */
  45. protected $_writer;
  46. /**
  47. * Tarball data reader
  48. *
  49. * @var File
  50. */
  51. protected $_reader;
  52. /**
  53. * Path to file where tarball should be placed
  54. *
  55. * @var string
  56. */
  57. protected $_destinationFilePath;
  58. /**
  59. * Initialize tarball writer
  60. *
  61. * @return $this
  62. */
  63. protected function _initWriter()
  64. {
  65. $this->_writer = new File($this->_destinationFilePath);
  66. $this->_writer->open('w');
  67. return $this;
  68. }
  69. /**
  70. * Returns string that is used for tar's header parsing
  71. *
  72. * @return string
  73. */
  74. protected static function _getFormatParseHeader()
  75. {
  76. return 'a100name/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1type/a100symlink/a6magic/a2version/' .
  77. 'a32uname/a32gname/a8devmajor/a8devminor/a155prefix/a12closer';
  78. }
  79. /**
  80. * Destroy tarball writer
  81. *
  82. * @return $this
  83. */
  84. protected function _destroyWriter()
  85. {
  86. if ($this->_writer instanceof File) {
  87. $this->_writer->close();
  88. $this->_writer = null;
  89. }
  90. return $this;
  91. }
  92. /**
  93. * Get tarball writer
  94. *
  95. * @return File
  96. */
  97. protected function _getWriter()
  98. {
  99. if (!$this->_writer) {
  100. $this->_initWriter();
  101. }
  102. return $this->_writer;
  103. }
  104. /**
  105. * Initialize tarball reader
  106. *
  107. * @return $this
  108. */
  109. protected function _initReader()
  110. {
  111. $this->_reader = new File($this->_getCurrentFile());
  112. $this->_reader->open('r');
  113. return $this;
  114. }
  115. /**
  116. * Destroy tarball reader
  117. *
  118. * @return $this
  119. */
  120. protected function _destroyReader()
  121. {
  122. if ($this->_reader instanceof File) {
  123. $this->_reader->close();
  124. $this->_reader = null;
  125. }
  126. return $this;
  127. }
  128. /**
  129. * Get tarball reader
  130. *
  131. * @return File
  132. */
  133. protected function _getReader()
  134. {
  135. if (!$this->_reader) {
  136. $this->_initReader();
  137. }
  138. return $this->_reader;
  139. }
  140. /**
  141. * Set option that define ability skip first catalog level.
  142. *
  143. * @param bool $skipRoot
  144. * @return $this
  145. */
  146. protected function _setSkipRoot($skipRoot)
  147. {
  148. $this->_skipRoot = $skipRoot;
  149. return $this;
  150. }
  151. /**
  152. * Set file which is packing.
  153. *
  154. * @param string $file
  155. * @return $this
  156. */
  157. protected function _setCurrentFile($file)
  158. {
  159. $file = str_replace('\\', '/', $file);
  160. $this->_currentFile = $file . (!is_link($file) && is_dir($file) && substr($file, -1) != '/' ? '/' : '');
  161. return $this;
  162. }
  163. /**
  164. * Set path to file where tarball should be placed
  165. *
  166. * @param string $destinationFilePath
  167. * @return $this
  168. */
  169. protected function _setDestinationFilePath($destinationFilePath)
  170. {
  171. $this->_destinationFilePath = $destinationFilePath;
  172. return $this;
  173. }
  174. /**
  175. * Retrieve file which is packing.
  176. *
  177. * @return string
  178. */
  179. protected function _getCurrentFile()
  180. {
  181. return $this->_currentFile;
  182. }
  183. /**
  184. * Set path to file which is packing.
  185. *
  186. * @param string $path
  187. * @return $this
  188. */
  189. protected function _setCurrentPath($path)
  190. {
  191. $path = str_replace('\\', '/', $path);
  192. if ($this->_skipRoot && is_dir($path)) {
  193. $this->_currentPath = $path . (substr($path, -1) != '/' ? '/' : '');
  194. } else {
  195. $this->_currentPath = dirname($path) . '/';
  196. }
  197. return $this;
  198. }
  199. /**
  200. * Retrieve path to file which is packing.
  201. *
  202. * @return string
  203. */
  204. protected function _getCurrentPath()
  205. {
  206. return $this->_currentPath;
  207. }
  208. /**
  209. * Recursively walk through file tree and create tarball
  210. *
  211. * @param bool $skipRoot
  212. * @param bool $finalize
  213. * @return void
  214. * @throws \Magento\Framework\Exception\LocalizedException
  215. */
  216. protected function _createTar($skipRoot = false, $finalize = false)
  217. {
  218. if (!$skipRoot) {
  219. $this->_packAndWriteCurrentFile();
  220. }
  221. $file = $this->_getCurrentFile();
  222. if (is_dir($file)) {
  223. $dirFiles = scandir($file, SCANDIR_SORT_NONE);
  224. if (false === $dirFiles) {
  225. throw new \Magento\Framework\Exception\LocalizedException(
  226. new \Magento\Framework\Phrase('Can\'t scan dir: %1', [$file])
  227. );
  228. }
  229. $dirFiles = array_diff($dirFiles, ['..', '.']);
  230. foreach ($dirFiles as $item) {
  231. $this->_setCurrentFile($file . $item)->_createTar();
  232. }
  233. }
  234. if ($finalize) {
  235. $this->_getWriter()->write(str_repeat("\0", self::TAR_BLOCK_SIZE * 12));
  236. }
  237. }
  238. /**
  239. * Write current file to tarball
  240. *
  241. * @return void
  242. */
  243. protected function _packAndWriteCurrentFile()
  244. {
  245. $archiveWriter = $this->_getWriter();
  246. $archiveWriter->write($this->_composeHeader());
  247. $currentFile = $this->_getCurrentFile();
  248. $fileSize = 0;
  249. if (is_file($currentFile) && !is_link($currentFile)) {
  250. $fileReader = new File($currentFile);
  251. $fileReader->open('r');
  252. while (!$fileReader->eof()) {
  253. $archiveWriter->write($fileReader->read());
  254. }
  255. $fileReader->close();
  256. $fileSize = filesize($currentFile);
  257. }
  258. $appendZerosCount = (self::TAR_BLOCK_SIZE - $fileSize % self::TAR_BLOCK_SIZE) % self::TAR_BLOCK_SIZE;
  259. $archiveWriter->write(str_repeat("\0", $appendZerosCount));
  260. }
  261. /**
  262. * Compose header for current file in TAR format.
  263. * If length of file's name greater 100 characters,
  264. * method breaks header into two pieces. First contains
  265. * header and data with long name. Second contain only header.
  266. *
  267. * @param bool $long
  268. * @return string
  269. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  270. * @SuppressWarnings(PHPMD.NPathComplexity)
  271. */
  272. protected function _composeHeader($long = false)
  273. {
  274. $file = $this->_getCurrentFile();
  275. $path = $this->_getCurrentPath();
  276. $infoFile = stat($file);
  277. $nameFile = str_replace($path, '', $file);
  278. $nameFile = str_replace('\\', '/', $nameFile);
  279. $packedHeader = '';
  280. $longHeader = '';
  281. if (!$long && strlen($nameFile) > 100) {
  282. $longHeader = $this->_composeHeader(true);
  283. $longHeader .= str_pad($nameFile, floor((strlen($nameFile) + 512 - 1) / 512) * 512, "\0");
  284. }
  285. $header = [];
  286. $header['100-name'] = $long ? '././@LongLink' : substr($nameFile, 0, 100);
  287. $header['8-mode'] = $long ? ' ' : str_pad(
  288. substr(sprintf("%07o", $infoFile['mode']), -4),
  289. 6,
  290. '0',
  291. STR_PAD_LEFT
  292. );
  293. $header['8-uid'] = $long || $infoFile['uid'] == 0 ? "\0\0\0\0\0\0\0" : sprintf("%07o", $infoFile['uid']);
  294. $header['8-gid'] = $long || $infoFile['gid'] == 0 ? "\0\0\0\0\0\0\0" : sprintf("%07o", $infoFile['gid']);
  295. $header['12-size'] = $long ? sprintf(
  296. "%011o",
  297. strlen($nameFile)
  298. ) : sprintf(
  299. "%011o",
  300. is_dir($file) ? 0 : filesize($file)
  301. );
  302. $header['12-mtime'] = $long ? '00000000000' : sprintf("%011o", $infoFile['mtime']);
  303. $header['8-check'] = sprintf('% 8s', '');
  304. $header['1-type'] = $long ? 'L' : (is_link($file) ? 2 : (is_dir($file) ? 5 : 0));
  305. $header['100-symlink'] = is_link($file) ? readlink($file) : '';
  306. $header['6-magic'] = 'ustar ';
  307. $header['2-version'] = ' ';
  308. $a = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner($file)) : ['name' => ''];
  309. $header['32-uname'] = $a['name'];
  310. $a = function_exists('posix_getgrgid') ? posix_getgrgid(filegroup($file)) : ['name' => ''];
  311. $header['32-gname'] = $a['name'];
  312. $header['8-devmajor'] = '';
  313. $header['8-devminor'] = '';
  314. $header['155-prefix'] = '';
  315. $header['12-closer'] = '';
  316. $packedHeader = '';
  317. foreach ($header as $key => $element) {
  318. $length = explode('-', $key);
  319. $packedHeader .= pack('a' . $length[0], $element);
  320. }
  321. $checksum = 0;
  322. for ($i = 0; $i < 512; $i++) {
  323. $checksum += ord(substr($packedHeader, $i, 1));
  324. }
  325. $packedHeader = substr_replace($packedHeader, sprintf("%07o", $checksum) . "\0", 148, 8);
  326. return $longHeader . $packedHeader;
  327. }
  328. /**
  329. * Read TAR string from file, and unpacked it.
  330. * Create files and directories information about described
  331. * in the string.
  332. *
  333. * @param string $destination path to file is unpacked
  334. * @return string[] list of files
  335. * @throws \Magento\Framework\Exception\LocalizedException
  336. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  337. */
  338. protected function _unpackCurrentTar($destination)
  339. {
  340. $archiveReader = $this->_getReader();
  341. $list = [];
  342. while (!$archiveReader->eof()) {
  343. $header = $this->_extractFileHeader();
  344. if (!$header) {
  345. continue;
  346. }
  347. $currentFile = $destination . $header['name'];
  348. $dirname = dirname($currentFile);
  349. if (in_array($header['type'], ["0", chr(0), ''])) {
  350. if (!file_exists($dirname)) {
  351. $mkdirResult = @mkdir($dirname, 0777, true);
  352. if (false === $mkdirResult) {
  353. throw new \Magento\Framework\Exception\LocalizedException(
  354. new \Magento\Framework\Phrase('Failed to create directory %1', [$dirname])
  355. );
  356. }
  357. }
  358. $this->_extractAndWriteFile($header, $currentFile);
  359. $list[] = $currentFile;
  360. } elseif ($header['type'] == '5') {
  361. if (!file_exists($dirname)) {
  362. $mkdirResult = @mkdir($currentFile, $header['mode'], true);
  363. if (false === $mkdirResult) {
  364. throw new \Magento\Framework\Exception\LocalizedException(
  365. new \Magento\Framework\Phrase('Failed to create directory %1', [$currentFile])
  366. );
  367. }
  368. }
  369. $list[] = $currentFile . '/';
  370. } elseif ($header['type'] == '2') {
  371. //we do not interrupt unpack process if symlink creation failed as symlinks are not so important
  372. @symlink($header['symlink'], $currentFile);
  373. }
  374. }
  375. return $list;
  376. }
  377. /**
  378. * Read and decode file header information from tarball
  379. *
  380. * @return array|bool
  381. */
  382. protected function _extractFileHeader()
  383. {
  384. $archiveReader = $this->_getReader();
  385. $headerBlock = $archiveReader->read(self::TAR_BLOCK_SIZE);
  386. if (strlen($headerBlock) < self::TAR_BLOCK_SIZE) {
  387. return false;
  388. }
  389. $header = unpack(self::_getFormatParseHeader(), $headerBlock);
  390. $header['mode'] = octdec($header['mode']);
  391. $header['uid'] = octdec($header['uid']);
  392. $header['gid'] = octdec($header['gid']);
  393. $header['size'] = octdec($header['size']);
  394. $header['mtime'] = octdec($header['mtime']);
  395. $header['checksum'] = octdec($header['checksum']);
  396. if ($header['type'] == "5") {
  397. $header['size'] = 0;
  398. }
  399. $checksum = 0;
  400. $headerBlock = substr_replace($headerBlock, ' ', 148, 8);
  401. for ($i = 0; $i < 512; $i++) {
  402. $checksum += ord(substr($headerBlock, $i, 1));
  403. }
  404. $checksumOk = $header['checksum'] == $checksum;
  405. if (isset($header['name']) && $checksumOk) {
  406. $header['name'] = trim($header['name']);
  407. if (!($header['name'] == '././@LongLink' && $header['type'] == 'L')) {
  408. return $header;
  409. }
  410. $realNameBlockSize = floor(
  411. ($header['size'] + self::TAR_BLOCK_SIZE - 1) / self::TAR_BLOCK_SIZE
  412. ) * self::TAR_BLOCK_SIZE;
  413. $realNameBlock = $archiveReader->read($realNameBlockSize);
  414. $realName = substr($realNameBlock, 0, $header['size']);
  415. $headerMain = $this->_extractFileHeader();
  416. $headerMain['name'] = trim($realName);
  417. return $headerMain;
  418. }
  419. return false;
  420. }
  421. /**
  422. * Extract next file from tarball by its $header information and save it to $destination
  423. *
  424. * @param array $fileHeader
  425. * @param string $destination
  426. * @return void
  427. */
  428. protected function _extractAndWriteFile($fileHeader, $destination)
  429. {
  430. $fileWriter = new File($destination);
  431. $fileWriter->open('w', $fileHeader['mode']);
  432. $archiveReader = $this->_getReader();
  433. $filesize = $fileHeader['size'];
  434. $bytesExtracted = 0;
  435. while ($filesize > $bytesExtracted && !$archiveReader->eof()) {
  436. $block = $archiveReader->read(self::TAR_BLOCK_SIZE);
  437. $nonExtractedBytesCount = $filesize - $bytesExtracted;
  438. $data = substr($block, 0, $nonExtractedBytesCount);
  439. $fileWriter->write($data);
  440. $bytesExtracted += strlen($block);
  441. }
  442. }
  443. /**
  444. * Pack file to TAR (Tape Archiver).
  445. *
  446. * @param string $source
  447. * @param string $destination
  448. * @param bool $skipRoot
  449. * @return string
  450. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  451. */
  452. public function pack($source, $destination, $skipRoot = false)
  453. {
  454. $this->_setSkipRoot($skipRoot);
  455. $source = realpath($source);
  456. $tarData = $this->_setCurrentPath($source)->_setDestinationFilePath($destination)->_setCurrentFile($source);
  457. $this->_initWriter();
  458. $this->_createTar($skipRoot, true);
  459. $this->_destroyWriter();
  460. return $destination;
  461. }
  462. /**
  463. * Unpack file from TAR (Tape Archiver).
  464. *
  465. * @param string $source
  466. * @param string $destination
  467. * @return string
  468. */
  469. public function unpack($source, $destination)
  470. {
  471. $this->_setCurrentFile($source)->_setCurrentPath($source);
  472. $this->_initReader();
  473. $this->_unpackCurrentTar($destination);
  474. $this->_destroyReader();
  475. return $destination;
  476. }
  477. /**
  478. * Extract one file from TAR (Tape Archiver).
  479. *
  480. * @param string $file
  481. * @param string $source
  482. * @param string $destination
  483. * @return string
  484. */
  485. public function extract($file, $source, $destination)
  486. {
  487. $this->_setCurrentFile($source);
  488. $this->_initReader();
  489. $archiveReader = $this->_getReader();
  490. $extractedFile = '';
  491. while (!$archiveReader->eof()) {
  492. $header = $this->_extractFileHeader();
  493. if ($header['name'] == $file) {
  494. $extractedFile = $destination . basename($header['name']);
  495. $this->_extractAndWriteFile($header, $extractedFile);
  496. break;
  497. }
  498. if ($header['type'] != 5) {
  499. $skipBytes = floor(
  500. ($header['size'] + self::TAR_BLOCK_SIZE - 1) / self::TAR_BLOCK_SIZE
  501. ) * self::TAR_BLOCK_SIZE;
  502. $archiveReader->read($skipBytes);
  503. }
  504. }
  505. $this->_destroyReader();
  506. return $extractedFile;
  507. }
  508. }