SymlinkDumper.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. <?php
  2. /*
  3. * This file is part of Packagist.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. * Nils Adermann <naderman@naderman.de>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Packagist\WebBundle\Package;
  12. use Symfony\Component\Filesystem\Filesystem;
  13. use Composer\Util\Filesystem as ComposerFilesystem;
  14. use Symfony\Bridge\Doctrine\RegistryInterface;
  15. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  16. use Symfony\Component\Finder\Finder;
  17. use Packagist\WebBundle\Entity\Version;
  18. use Doctrine\DBAL\Connection;
  19. /**
  20. * @author Jordi Boggiano <j.boggiano@seld.be>
  21. */
  22. class SymlinkDumper
  23. {
  24. /**
  25. * Doctrine
  26. * @var RegistryInterface
  27. */
  28. protected $doctrine;
  29. /**
  30. * @var Filesystem
  31. */
  32. protected $fs;
  33. /**
  34. * @var ComposerFilesystem
  35. */
  36. protected $cfs;
  37. /**
  38. * @var string
  39. */
  40. protected $webDir;
  41. /**
  42. * @var string
  43. */
  44. protected $buildDir;
  45. /**
  46. * @var UrlGeneratorInterface
  47. */
  48. protected $router;
  49. /**
  50. * Data cache
  51. * @var array
  52. */
  53. private $rootFile;
  54. /**
  55. * Data cache
  56. * @var array
  57. */
  58. private $listings = array();
  59. /**
  60. * Data cache
  61. * @var array
  62. */
  63. private $individualFiles = array();
  64. /**
  65. * Modified times of individual files
  66. * @var array
  67. */
  68. private $individualFilesMtime = array();
  69. /**
  70. * Stores all the disk writes to be replicated in the second build dir after the symlink has been swapped
  71. * @var array
  72. */
  73. private $writeLog = array();
  74. /**
  75. * Generate compressed files.
  76. * @var int 0 disabled, 9 maximum.
  77. */
  78. private $compress;
  79. /**
  80. * Constructor
  81. *
  82. * @param RegistryInterface $doctrine
  83. * @param Filesystem $filesystem
  84. * @param UrlGeneratorInterface $router
  85. * @param string $webDir web root
  86. * @param string $targetDir
  87. * @param int $compress
  88. */
  89. public function __construct(RegistryInterface $doctrine, Filesystem $filesystem, UrlGeneratorInterface $router, $webDir, $targetDir, $compress)
  90. {
  91. $this->doctrine = $doctrine;
  92. $this->fs = $filesystem;
  93. $this->cfs = new ComposerFilesystem;
  94. $this->router = $router;
  95. $this->webDir = realpath($webDir);
  96. $this->buildDir = $targetDir;
  97. $this->compress = $compress;
  98. }
  99. /**
  100. * Dump a set of packages to the web root
  101. *
  102. * @param array $packageIds
  103. * @param Boolean $force
  104. * @param Boolean $verbose
  105. */
  106. public function dump(array $packageIds, $force = false, $verbose = false)
  107. {
  108. $cleanUpOldFiles = date('i') == 0;
  109. // prepare build dir
  110. $webDir = $this->webDir;
  111. $buildDirA = $this->buildDir.'/a';
  112. $buildDirB = $this->buildDir.'/b';
  113. // initialize
  114. $initialRun = false;
  115. if (!is_dir($buildDirA) || !is_dir($buildDirB)) {
  116. $initialRun = true;
  117. if (!$this->removeDirectory($buildDirA) || !$this->removeDirectory($buildDirB)) {
  118. throw new \RuntimeException('Failed to delete '.$buildDirA.' or '.$buildDirB);
  119. }
  120. $this->fs->mkdir($buildDirA);
  121. $this->fs->mkdir($buildDirB);
  122. }
  123. // set build dir to the not-active one
  124. if (realpath($webDir.'/p') === realpath($buildDirA)) {
  125. $buildDir = realpath($buildDirB);
  126. $oldBuildDir = realpath($buildDirA);
  127. } else {
  128. $buildDir = realpath($buildDirA);
  129. $oldBuildDir = realpath($buildDirB);
  130. }
  131. // copy existing stuff for smooth BC transition
  132. if ($initialRun && !$force) {
  133. if (!file_exists($webDir.'/p') || is_link($webDir.'/p')) {
  134. @rmdir($buildDir);
  135. @rmdir($oldBuildDir);
  136. throw new \RuntimeException('Run this again with --force the first time around to make sure it dumps all packages');
  137. }
  138. if ($verbose) {
  139. echo 'Copying existing files'.PHP_EOL;
  140. }
  141. foreach (array($buildDir, $oldBuildDir) as $dir) {
  142. $this->cloneDir($webDir.'/p', $dir);
  143. }
  144. }
  145. if ($verbose) {
  146. echo 'Web dir is '.$webDir.'/p ('.realpath($webDir.'/p').')'.PHP_EOL;
  147. echo 'Build dir is '.$buildDir.PHP_EOL;
  148. }
  149. // clean the build dir to start over if we are re-dumping everything
  150. if ($force) {
  151. // disable the write log since we copy everything at the end in forced mode
  152. $this->writeLog = false;
  153. if ($verbose) {
  154. echo 'Cleaning up existing files'.PHP_EOL;
  155. }
  156. if (!$this->clearDirectory($buildDir)) {
  157. return false;
  158. }
  159. }
  160. $dumpTimeUpdates = [];
  161. $versionRepo = $this->doctrine->getRepository('PackagistWebBundle:Version');
  162. try {
  163. $modifiedIndividualFiles = array();
  164. $total = count($packageIds);
  165. $current = 0;
  166. $step = 50;
  167. while ($packageIds) {
  168. $dumpTime = new \DateTime;
  169. $packages = $this->doctrine->getRepository('PackagistWebBundle:Package')->getPackagesWithVersions(array_splice($packageIds, 0, $step));
  170. if ($verbose) {
  171. echo '['.sprintf('%'.strlen($total).'d', $current).'/'.$total.'] Processing '.$step.' packages'.PHP_EOL;
  172. }
  173. $current += $step;
  174. // prepare packages in memory
  175. foreach ($packages as $package) {
  176. // skip spam packages in the dumper in case we do a forced full dump and prevent them from being dumped for a little while
  177. if ($package->isAbandoned() && $package->getReplacementPackage() === 'spam/spam') {
  178. $dumpTimeUpdates['2100-01-01 00:00:00'][] = $package->getId();
  179. continue;
  180. }
  181. $affectedFiles = array();
  182. $name = strtolower($package->getName());
  183. // clean up versions in individual files
  184. if (file_exists($buildDir.'/'.$name.'.files')) {
  185. $files = json_decode(file_get_contents($buildDir.'/'.$name.'.files'));
  186. foreach ($files as $file) {
  187. if (substr_count($file, '/') > 1) { // handle old .files with p/*/*.json paths
  188. $file = preg_replace('{^p/}', '', $file);
  189. }
  190. $this->loadIndividualFile($buildDir.'/'.$file, $file);
  191. if (isset($this->individualFiles[$file]['packages'][$name])) {
  192. unset($this->individualFiles[$file]['packages'][$name]);
  193. $modifiedIndividualFiles[$file] = true;
  194. }
  195. }
  196. }
  197. // (re)write versions in individual files
  198. $versionIds = [];
  199. foreach ($package->getVersions() as $version) {
  200. $versionIds[] = $version->getId();
  201. }
  202. $versionData = $versionRepo->getVersionData($versionIds);
  203. foreach ($package->getVersions() as $version) {
  204. foreach (array_slice($version->getNames(), 0, 150) as $versionName) {
  205. if (!preg_match('{^[A-Za-z0-9_-][A-Za-z0-9_.-]*/[A-Za-z0-9_-][A-Za-z0-9_.-]*$}', $versionName) || strpos($versionName, '..')) {
  206. continue;
  207. }
  208. $file = $buildDir.'/'.$versionName.'.json';
  209. $key = $versionName.'.json';
  210. $this->dumpVersionToIndividualFile($version, $file, $key, $versionData);
  211. $modifiedIndividualFiles[$key] = true;
  212. $affectedFiles[$key] = true;
  213. }
  214. }
  215. // store affected files to clean up properly in the next update
  216. $this->fs->mkdir(dirname($buildDir.'/'.$name));
  217. $this->writeFile($buildDir.'/'.$name.'.files', json_encode(array_keys($affectedFiles)));
  218. $dumpTimeUpdates[$dumpTime->format('Y-m-d H:i:s')][] = $package->getId();
  219. }
  220. unset($packages, $package, $version);
  221. $this->doctrine->getManager()->clear();
  222. if ($current % 250 === 0 || !$packageIds) {
  223. if ($verbose) {
  224. echo 'Dumping individual files'.PHP_EOL;
  225. }
  226. $this->dumpIndividualFiles($buildDir);
  227. }
  228. }
  229. // prepare individual files listings
  230. if ($verbose) {
  231. echo 'Preparing individual files listings'.PHP_EOL;
  232. }
  233. $safeFiles = array();
  234. $individualHashedListings = array();
  235. $finder = Finder::create()->files()->ignoreVCS(true)->name('*.json')->in($buildDir)->depth('1');
  236. foreach ($finder as $file) {
  237. // skip hashed files
  238. if (strpos($file, '$')) {
  239. continue;
  240. }
  241. $key = basename(dirname($file)).'/'.basename($file);
  242. if ($force && !isset($modifiedIndividualFiles[$key])) {
  243. continue;
  244. }
  245. // add hashed provider to listing
  246. $listing = $this->getTargetListing($file);
  247. $hash = hash_file('sha256', $file);
  248. $key = substr($key, 0, -5);
  249. $safeFiles[] = $key.'$'.$hash.'.json';
  250. $this->listings[$listing]['providers'][$key] = array('sha256' => $hash);
  251. $individualHashedListings[$listing] = true;
  252. }
  253. // prepare root file
  254. $rootFile = $buildDir.'/packages.json';
  255. $this->rootFile = array('packages' => array());
  256. $url = $this->router->generate('track_download', array('name' => 'VND/PKG'));
  257. $this->rootFile['notify'] = str_replace('VND/PKG', '%package%', $url);
  258. $this->rootFile['notify-batch'] = $this->router->generate('track_download_batch');
  259. $this->rootFile['providers-url'] = $this->router->generate('home') . 'p/%package%$%hash%.json';
  260. $this->rootFile['search'] = $this->router->generate('search', array('_format' => 'json')) . '?q=%query%&type=%type%';
  261. if ($verbose) {
  262. echo 'Dumping individual listings'.PHP_EOL;
  263. }
  264. // dump listings to build dir
  265. foreach ($individualHashedListings as $listing => $dummy) {
  266. list($listingPath, $hash) = $this->dumpListing($buildDir.'/'.$listing);
  267. $hashedListing = basename($listingPath);
  268. $this->rootFile['provider-includes']['p/'.str_replace($hash, '%hash%', $hashedListing)] = array('sha256' => $hash);
  269. $safeFiles[] = $hashedListing;
  270. }
  271. if ($verbose) {
  272. echo 'Dumping root'.PHP_EOL;
  273. }
  274. $this->dumpRootFile($rootFile);
  275. } catch (\Exception $e) {
  276. // restore files as they were before we started
  277. $this->cloneDir($oldBuildDir, $buildDir);
  278. throw $e;
  279. }
  280. try {
  281. if ($verbose) {
  282. echo 'Putting new files in production'.PHP_EOL;
  283. }
  284. // move away old files for BC update
  285. if ($initialRun && file_exists($webDir.'/p') && !is_link($webDir.'/p')) {
  286. rename($webDir.'/p', $webDir.'/p-old');
  287. }
  288. $this->switchActiveWebDir($webDir, $buildDir);
  289. } catch (\Exception $e) {
  290. @symlink($oldBuildDir, $webDir.'/p');
  291. throw $e;
  292. }
  293. try {
  294. if ($initialRun || !is_link($webDir.'/packages.json') || $force) {
  295. if ($verbose) {
  296. echo 'Writing/linking the packages.json'.PHP_EOL;
  297. }
  298. if (file_exists($webDir.'/packages.json')) {
  299. unlink($webDir.'/packages.json');
  300. }
  301. if (file_exists($webDir.'/packages.json.gz')) {
  302. unlink($webDir.'/packages.json.gz');
  303. }
  304. if (defined('PHP_WINDOWS_VERSION_BUILD')) {
  305. $sourcePath = $buildDir.'/packages.json';
  306. if (!copy($sourcePath, $webDir.'/packages.json')) {
  307. throw new \RuntimeException('Could not copy the packages.json file');
  308. }
  309. } else {
  310. $sourcePath = 'p/packages.json';
  311. if (!symlink($sourcePath, $webDir.'/packages.json')) {
  312. throw new \RuntimeException('Could not symlink the packages.json file');
  313. }
  314. if ($this->compress && !symlink($sourcePath.'.gz', $webDir.'/packages.json.gz')) {
  315. throw new \RuntimeException('Could not symlink the packages.json.gz file');
  316. }
  317. }
  318. }
  319. } catch (\Exception $e) {
  320. $this->switchActiveWebDir($webDir, $oldBuildDir);
  321. throw $e;
  322. }
  323. // clean up old dir if present on BC update
  324. if ($initialRun) {
  325. $this->removeDirectory($webDir.'/p-old');
  326. }
  327. // clean the old build dir if we re-dumped everything
  328. if ($force) {
  329. if ($verbose) {
  330. echo 'Cleaning up old build dir'.PHP_EOL;
  331. }
  332. if (!$this->clearDirectory($oldBuildDir)) {
  333. throw new \RuntimeException('Unrecoverable inconsistent state (old build dir could not be cleared), run with --force again to retry');
  334. }
  335. }
  336. // copy state to old active dir
  337. if ($force) {
  338. if ($verbose) {
  339. echo 'Copying new contents to old build dir to sync up'.PHP_EOL;
  340. }
  341. $this->cloneDir($buildDir, $oldBuildDir);
  342. } else {
  343. if ($verbose) {
  344. echo 'Replaying write log in old build dir'.PHP_EOL;
  345. }
  346. $this->copyWriteLog($buildDir, $oldBuildDir);
  347. }
  348. // clean up old files once an hour
  349. if (!$force && $cleanUpOldFiles) {
  350. if ($verbose) {
  351. echo 'Cleaning up old files'.PHP_EOL;
  352. }
  353. $this->cleanOldFiles($buildDir, $oldBuildDir, $safeFiles);
  354. }
  355. if ($verbose) {
  356. echo 'Updating package dump times'.PHP_EOL;
  357. }
  358. foreach ($dumpTimeUpdates as $dt => $ids) {
  359. $retries = 5;
  360. // retry loop in case of a lock timeout
  361. while ($retries--) {
  362. try {
  363. $this->doctrine->getManager()->getConnection()->executeQuery(
  364. 'UPDATE package SET dumpedAt=:dumped WHERE id IN (:ids)',
  365. [
  366. 'ids' => $ids,
  367. 'dumped' => $dt,
  368. ],
  369. ['ids' => Connection::PARAM_INT_ARRAY]
  370. );
  371. } catch (\Exception $e) {
  372. if (!$retries) {
  373. throw $e;
  374. }
  375. sleep(2);
  376. }
  377. }
  378. }
  379. // TODO when a package is deleted, it should be removed from provider files, or marked for removal at least
  380. return true;
  381. }
  382. private function switchActiveWebDir($webDir, $buildDir)
  383. {
  384. $newLink = $webDir.'/p-new';
  385. $oldLink = $webDir.'/p';
  386. if (file_exists($newLink)) {
  387. unlink($newLink);
  388. }
  389. if (!symlink($buildDir, $newLink)) {
  390. echo 'Warning: Could not symlink the build dir into the web dir';
  391. throw new \RuntimeException('Could not symlink the build dir into the web dir');
  392. }
  393. if (!rename($newLink, $oldLink)) {
  394. echo 'Warning: Could not replace the old symlink with the new one in the web dir';
  395. throw new \RuntimeException('Could not replace the old symlink with the new one in the web dir');
  396. }
  397. }
  398. private function cloneDir($source, $target)
  399. {
  400. $this->removeDirectory($target);
  401. exec('cp -rpf '.escapeshellarg($source).' '.escapeshellarg($target), $output, $exit);
  402. if (0 !== $exit) {
  403. echo 'Warning, cloning a directory using the php fallback does not keep filemtime, invalid behavior may occur';
  404. $this->fs->mirror($source, $target, null, array('override' => true));
  405. }
  406. }
  407. private function cleanOldFiles($buildDir, $oldBuildDir, $safeFiles)
  408. {
  409. $finder = Finder::create()->directories()->ignoreVCS(true)->in($buildDir);
  410. foreach ($finder as $vendorDir) {
  411. $vendorFiles = Finder::create()->files()->ignoreVCS(true)
  412. ->name('/\$[a-f0-9]+\.json$/')
  413. ->date('until 10minutes ago')
  414. ->in((string) $vendorDir);
  415. foreach ($vendorFiles as $file) {
  416. $key = strtr(str_replace($buildDir.DIRECTORY_SEPARATOR, '', $file), '\\', '/');
  417. if (!in_array($key, $safeFiles, true)) {
  418. unlink((string) $file);
  419. if (file_exists($altDirFile = str_replace($buildDir, $oldBuildDir, (string) $file))) {
  420. unlink($altDirFile);
  421. }
  422. }
  423. }
  424. }
  425. // clean up old provider listings
  426. $finder = Finder::create()->depth(0)->files()->name('provider-*.json')->ignoreVCS(true)->in($buildDir)->date('until 10minutes ago');
  427. foreach ($finder as $provider) {
  428. $key = strtr(str_replace($buildDir.DIRECTORY_SEPARATOR, '', $provider), '\\', '/');
  429. if (!in_array($key, $safeFiles, true)) {
  430. $path = (string) $provider;
  431. unlink($path);
  432. if (file_exists($path.'.gz')) {
  433. unlink($path.'.gz');
  434. }
  435. if (file_exists($altDirFile = str_replace($buildDir, $oldBuildDir, $path))) {
  436. unlink($altDirFile);
  437. if (file_exists($altDirFile.'.gz')) {
  438. unlink($altDirFile.'.gz');
  439. }
  440. }
  441. }
  442. }
  443. // clean up old root listings
  444. $finder = Finder::create()->depth(0)->files()->name('packages.json-*')->ignoreVCS(true)->in($buildDir)->date('until 10minutes ago');
  445. foreach ($finder as $rootFile) {
  446. $path = (string) $rootFile;
  447. unlink($path);
  448. if (file_exists($path.'.gz')) {
  449. unlink($path.'.gz');
  450. }
  451. if (file_exists($altDirFile = str_replace($buildDir, $oldBuildDir, $path))) {
  452. unlink($altDirFile);
  453. if (file_exists($altDirFile.'.gz')) {
  454. unlink($altDirFile.'.gz');
  455. }
  456. }
  457. }
  458. }
  459. private function dumpRootFile($file)
  460. {
  461. // sort all versions and packages to make sha1 consistent
  462. ksort($this->rootFile['packages']);
  463. ksort($this->rootFile['provider-includes']);
  464. foreach ($this->rootFile['packages'] as $package => $versions) {
  465. ksort($this->rootFile['packages'][$package]);
  466. }
  467. if (file_exists($file)) {
  468. $timedFile = $file.'-'.time();
  469. rename($file, $timedFile);
  470. if (file_exists($file.'.gz')) {
  471. rename($file.'.gz', $timedFile.'.gz');
  472. }
  473. }
  474. $json = json_encode($this->rootFile);
  475. $time = time();
  476. $this->writeFile($file, $json, $time);
  477. if ($this->compress) {
  478. $this->writeFile($file . '.gz', gzencode($json, $this->compress), $time);
  479. }
  480. }
  481. private function dumpListing($path)
  482. {
  483. $key = basename($path);
  484. // sort files to make hash consistent
  485. ksort($this->listings[$key]['providers']);
  486. $json = json_encode($this->listings[$key]);
  487. $hash = hash('sha256', $json);
  488. $path = substr($path, 0, -5) . '$' . $hash . '.json';
  489. $time = time();
  490. if (!file_exists($path)) {
  491. $this->writeFile($path, $json, $time);
  492. if ($this->compress) {
  493. $this->writeFile($path . '.gz', gzencode($json, $this->compress), $time);
  494. }
  495. }
  496. return array($path, $hash);
  497. }
  498. private function loadIndividualFile($path, $key)
  499. {
  500. if (isset($this->individualFiles[$key])) {
  501. return;
  502. }
  503. if (file_exists($path)) {
  504. $this->individualFiles[$key] = json_decode(file_get_contents($path), true);
  505. $this->individualFilesMtime[$key] = filemtime($path);
  506. } else {
  507. $this->individualFiles[$key] = array();
  508. $this->individualFilesMtime[$key] = 0;
  509. }
  510. }
  511. private function dumpIndividualFiles($buildDir)
  512. {
  513. // dump individual files to build dir
  514. foreach ($this->individualFiles as $file => $dummy) {
  515. $this->dumpIndividualFile($buildDir.'/'.$file, $file);
  516. }
  517. $this->individualFiles = array();
  518. $this->individualFilesMtime = array();
  519. }
  520. private function dumpIndividualFile($path, $key)
  521. {
  522. // sort all versions and packages to make sha1 consistent
  523. ksort($this->individualFiles[$key]['packages']);
  524. foreach ($this->individualFiles[$key]['packages'] as $package => $versions) {
  525. ksort($this->individualFiles[$key]['packages'][$package]);
  526. }
  527. $this->fs->mkdir(dirname($path));
  528. $json = json_encode($this->individualFiles[$key]);
  529. $this->writeFile($path, $json, $this->individualFilesMtime[$key]);
  530. // write the hashed provider file
  531. $hashedFile = substr($path, 0, -5) . '$' . hash('sha256', $json) . '.json';
  532. $this->writeFile($hashedFile, $json);
  533. }
  534. private function dumpVersionToIndividualFile(Version $version, $file, $key, $versionData)
  535. {
  536. $this->loadIndividualFile($file, $key);
  537. $data = $version->toArray($versionData);
  538. $data['uid'] = $version->getId();
  539. $this->individualFiles[$key]['packages'][strtolower($version->getName())][$version->getVersion()] = $data;
  540. $timestamp = $version->getReleasedAt() ? $version->getReleasedAt()->getTimestamp() : time();
  541. if (!isset($this->individualFilesMtime[$key]) || $this->individualFilesMtime[$key] < $timestamp) {
  542. $this->individualFilesMtime[$key] = $timestamp;
  543. }
  544. }
  545. private function clearDirectory($path)
  546. {
  547. if (!$this->removeDirectory($path)) {
  548. echo 'Could not remove the build dir entirely, aborting';
  549. return false;
  550. }
  551. $this->fs->mkdir($path);
  552. return true;
  553. }
  554. private function removeDirectory($path)
  555. {
  556. $retries = 5;
  557. do {
  558. if (!$this->cfs->removeDirectory($path)) {
  559. usleep(200);
  560. }
  561. clearstatcache();
  562. } while (is_dir($path) && $retries--);
  563. return !is_dir($path);
  564. }
  565. private function getTargetListingBlocks($now)
  566. {
  567. $blocks = array();
  568. // monday last week
  569. $blocks['latest'] = strtotime('monday last week', $now);
  570. $month = date('n', $now);
  571. $month = ceil($month / 3) * 3 - 2; // 1 for months 1-3, 10 for months 10-12
  572. $block = new \DateTime(date('Y', $now).'-'.$month.'-01'); // 1st day of current trimester
  573. // split last 12 months in 4 trimesters
  574. for ($i=0; $i < 4; $i++) {
  575. $blocks[$block->format('Y-m')] = $block->getTimestamp();
  576. $block->sub(new \DateInterval('P3M'));
  577. }
  578. $year = (int) $block->format('Y');
  579. while ($year >= 2013) {
  580. $blocks[''.$year] = strtotime($year.'-01-01');
  581. $year--;
  582. }
  583. return $blocks;
  584. }
  585. private function getTargetListing($file)
  586. {
  587. static $blocks;
  588. if (!$blocks) {
  589. $blocks = $this->getTargetListingBlocks(time());
  590. }
  591. $mtime = filemtime($file);
  592. foreach ($blocks as $label => $block) {
  593. if ($mtime >= $block) {
  594. return "provider-${label}.json";
  595. }
  596. }
  597. return "provider-archived.json";
  598. }
  599. private function writeFile($path, $contents, $mtime = null)
  600. {
  601. file_put_contents($path, $contents);
  602. if ($mtime !== null) {
  603. touch($path, $mtime);
  604. }
  605. if (is_array($this->writeLog)) {
  606. $this->writeLog[$path] = array($contents, $mtime);
  607. }
  608. }
  609. private function copyWriteLog($from, $to)
  610. {
  611. foreach ($this->writeLog as $path => $op) {
  612. $path = str_replace($from, $to, $path);
  613. $this->fs->mkdir(dirname($path));
  614. file_put_contents($path, $op[0]);
  615. if ($op[1] !== null) {
  616. touch($path, $op[1]);
  617. }
  618. }
  619. }
  620. }