1. sfFilesystem.class.php
  2. /** * sfFilesystem provides basic utility to manipulate the file system. * * @package symfony * @subpackage util * @author Fabien Potencier * @version SVN: $Id: sfFilesystem.class.php 24991 2009-12-06 20:22:52Z Kris.Wallsmith $ */
  3. class sfFilesystem
  4. {
  5. protected
  6. $dispatcher = null,
  7. $formatter = null;
  8. /**
  9. * Constructor.
  10. *
  11. * @param sfEventDispatcher $dispatcher An sfEventDispatcher instance
  12. * @param sfFormatter $formatter An sfFormatter instance
  13. */
  14. public function __construct(sfEventDispatcher $dispatcher = null, sfFormatter $formatter = null)
  15. {
  16. $this->dispatcher = $dispatcher;
  17. $this->formatter = $formatter;
  18. }
  19. /**
  20. * Copies a file.
  21. *
  22. * This method only copies the file if the origin file is newer than the target file.
  23. *
  24. * By default, if the target already exists, it is not overriden.
  25. *
  26. * To override existing files, pass the "override" option.
  27. *
  28. * @param string $originFile The original filename
  29. * @param string $targetFile The target filename
  30. * @param array $options An array of options
  31. */
  32. public function copy($originFile, $targetFile, $options = array())
  33. {
  34. if (!array_key_exists('override', $options))
  35. {
  36. $options['override'] = false;
  37. }
  38. // we create target_dir if needed
  39. if (!is_dir(dirname($targetFile)))
  40. {
  41. $this->mkdirs(dirname($targetFile));
  42. }
  43. $mostRecent = false;
  44. if (file_exists($targetFile))
  45. {
  46. $statTarget = stat($targetFile);
  47. $stat_origin = stat($originFile);
  48. $mostRecent = ($stat_origin['mtime'] > $statTarget['mtime']) ? true : false;
  49. }
  50. if ($options['override'] || !file_exists($targetFile) || $mostRecent)
  51. {
  52. $this->logSection('file+', $targetFile);
  53. copy($originFile, $targetFile);
  54. }
  55. }
  56. /**
  57. * Creates a directory recursively.
  58. *
  59. * @param string $path The directory path
  60. * @param int $mode The directory mode
  61. *
  62. * @return bool true if the directory has been created, false otherwise
  63. */
  64. public function mkdirs($path, $mode = 0777)
  65. {
  66. if (is_dir($path))
  67. {
  68. return true;
  69. }
  70. $this->logSection('dir+', $path);
  71. return @mkdir($path, $mode, true);
  72. }
  73. /**
  74. * Creates empty files.
  75. *
  76. * @param mixed $files The filename, or an array of filenames
  77. */
  78. public function touch($files)
  79. {
  80. if (!is_array($files))
  81. {
  82. $files = array($files);
  83. }
  84. foreach ($files as $file)
  85. {
  86. $this->logSection('file+', $file);
  87. touch($file);
  88. }
  89. }
  90. /**
  91. * Removes files or directories.
  92. *
  93. * @param mixed $files A filename or an array of files to remove
  94. */
  95. public function remove($files)
  96. {
  97. if (!is_array($files))
  98. {
  99. $files = array($files);
  100. }
  101. $files = array_reverse($files);
  102. foreach ($files as $file)
  103. {
  104. if (is_dir($file) && !is_link($file))
  105. {
  106. $this->logSection('dir-', $file);
  107. rmdir($file);
  108. }
  109. else
  110. {
  111. $this->logSection(is_link($file) ? 'link-' : 'file-', $file);
  112. unlink($file);
  113. }
  114. }
  115. }
  116. /**
  117. * Change mode for an array of files or directories.
  118. *
  119. * @param array $files An array of files or directories
  120. * @param integer $mode The new mode
  121. * @param integer $umask The mode mask (octal)
  122. */
  123. public function chmod($files, $mode, $umask = 0000)
  124. {
  125. $currentUmask = umask();
  126. umask($umask);
  127. if (!is_array($files))
  128. {
  129. $files = array($files);
  130. }
  131. foreach ($files as $file)
  132. {
  133. $this->logSection(sprintf('chmod %o', $mode), $file);
  134. chmod($file, $mode);
  135. }
  136. umask($currentUmask);
  137. }
  138. /**
  139. * Renames a file.
  140. *
  141. * @param string $origin The origin filename
  142. * @param string $target The new filename
  143. */
  144. public function rename($origin, $target)
  145. {
  146. // we check that target does not exist
  147. if (is_readable($target))
  148. {
  149. throw new sfException(sprintf('Cannot rename because the target "%" already exist.', $target));
  150. }
  151. $this->logSection('rename', $origin.' > '.$target);
  152. rename($origin, $target);
  153. }
  154. /**
  155. * Creates a symbolic link or copy a directory.
  156. *
  157. * @param string $originDir The origin directory path
  158. * @param string $targetDir The symbolic link name
  159. * @param bool $copyOnWindows Whether to copy files if on windows
  160. */
  161. public function symlink($originDir, $targetDir, $copyOnWindows = false)
  162. {
  163. if (!function_exists('symlink') && $copyOnWindows)
  164. {
  165. $finder = sfFinder::type('any');
  166. $this->mirror($originDir, $targetDir, $finder);
  167. return;
  168. }
  169. $ok = false;
  170. if (is_link($targetDir))
  171. {
  172. if (readlink($targetDir) != $originDir)
  173. {
  174. unlink($targetDir);
  175. }
  176. else
  177. {
  178. $ok = true;
  179. }
  180. }
  181. if (!$ok)
  182. {
  183. $this->logSection('link+', $targetDir);
  184. symlink($originDir, $targetDir);
  185. }
  186. }
  187. /**
  188. * Creates a symbolic link using a relative path if possible.
  189. *
  190. * @param string $originDir The origin directory path
  191. * @param string $targetDir The symbolic link name
  192. * @param bool $copyOnWindows Whether to copy files if on windows
  193. */
  194. public function relativeSymlink($originDir, $targetDir, $copyOnWindows = false)
  195. {
  196. if (function_exists('symlink') || !$copyOnWindows)
  197. {
  198. $originDir = $this->calculateRelativeDir($targetDir, $originDir);
  199. }
  200. $this->symlink($originDir, $targetDir, $copyOnWindows);
  201. }
  202. /**
  203. * Mirrors a directory to another.
  204. *
  205. * @param string $originDir The origin directory
  206. * @param string $targetDir The target directory
  207. * @param sfFinder $finder An sfFinder instance
  208. * @param array $options An array of options (see copy())
  209. */
  210. public function mirror($originDir, $targetDir, $finder, $options = array())
  211. {
  212. foreach ($finder->relative()->in($originDir) as $file)
  213. {
  214. if (is_dir($originDir.DIRECTORY_SEPARATOR.$file))
  215. {
  216. $this->mkdirs($targetDir.DIRECTORY_SEPARATOR.$file);
  217. }
  218. else if (is_file($originDir.DIRECTORY_SEPARATOR.$file))
  219. {
  220. $this->copy($originDir.DIRECTORY_SEPARATOR.$file, $targetDir.DIRECTORY_SEPARATOR.$file, $options);
  221. }
  222. else if (is_link($originDir.DIRECTORY_SEPARATOR.$file))
  223. {
  224. $this->symlink($originDir.DIRECTORY_SEPARATOR.$file, $targetDir.DIRECTORY_SEPARATOR.$file);
  225. }
  226. else
  227. {
  228. throw new sfException(sprintf('Unable to guess "%s" file type.', $file));
  229. }
  230. }
  231. }
  232. /**
  233. * Executes a shell command.
  234. *
  235. * @param string $cmd The command to execute on the shell
  236. * @param array $stdoutCallback A callback for stdout output
  237. * @param array $stderrCallback A callback for stderr output
  238. *
  239. * @return array An array composed of the content output and the error output
  240. */
  241. public function execute($cmd, $stdoutCallback = null, $stderrCallback = null)
  242. {
  243. $this->logSection('exec ', $cmd);
  244. $descriptorspec = array(
  245. 1 => array('pipe', 'w'), // stdout
  246. 2 => array('pipe', 'w'), // stderr
  247. );
  248. $process = proc_open($cmd, $descriptorspec, $pipes);
  249. if (!is_resource($process))
  250. {
  251. throw new RuntimeException('Unable to execute the command.');
  252. }
  253. stream_set_blocking($pipes[1], false);
  254. stream_set_blocking($pipes[2], false);
  255. $output = '';
  256. $err = '';
  257. while (!feof($pipes[1]))
  258. {
  259. foreach ($pipes as $key => $pipe)
  260. {
  261. if (!$line = fread($pipe, 128))
  262. {
  263. continue;
  264. }
  265. if (1 == $key)
  266. {
  267. // stdout
  268. $output .= $line;
  269. if ($stdoutCallback)
  270. {
  271. call_user_func($stdoutCallback, $line);
  272. }
  273. }
  274. else
  275. {
  276. // stderr
  277. $err .= $line;
  278. if ($stderrCallback)
  279. {
  280. call_user_func($stderrCallback, $line);
  281. }
  282. }
  283. }
  284. sleep(0.1);
  285. }
  286. fclose($pipes[1]);
  287. fclose($pipes[2]);
  288. if (($return = proc_close($process)) > 0)
  289. {
  290. throw new RuntimeException('Problem executing command.', $return);
  291. }
  292. return array($output, $err);
  293. }
  294. /**
  295. * Replaces tokens in an array of files.
  296. *
  297. * @param array $files An array of filenames
  298. * @param string $beginToken The begin token delimiter
  299. * @param string $endToken The end token delimiter
  300. * @param array $tokens An array of token/value pairs
  301. */
  302. public function replaceTokens($files, $beginToken, $endToken, $tokens)
  303. {
  304. if (!is_array($files))
  305. {
  306. $files = array($files);
  307. }
  308. foreach ($files as $file)
  309. {
  310. $content = file_get_contents($file);
  311. foreach ($tokens as $key => $value)
  312. {
  313. $content = str_replace($beginToken.$key.$endToken, $value, $content, $count);
  314. }
  315. $this->logSection('tokens', $file);
  316. file_put_contents($file, $content);
  317. }
  318. }
  319. /**
  320. * Logs a message in a section.
  321. *
  322. * @param string $section The section name
  323. * @param string $message The message
  324. * @param int $size The maximum size of a line
  325. */
  326. protected function logSection($section, $message, $size = null)
  327. {
  328. if (!$this->dispatcher)
  329. {
  330. return;
  331. }
  332. $message = $this->formatter ? $this->formatter->formatSection($section, $message, $size) : $section.' '.$message."\n";
  333. $this->dispatcher->notify(new sfEvent($this, 'command.log', array($message)));
  334. }
  335. /**
  336. * Calculates the relative path from one to another directory.
  337. *
  338. * If the paths share no common path the absolute target dir is returned.
  339. *
  340. * @param string $from The directory from which to calculate the relative path
  341. * @param string $to The target directory
  342. *
  343. * @return string
  344. */
  345. protected function calculateRelativeDir($from, $to)
  346. {
  347. $from = $this->canonicalizePath($from);
  348. $to = $this->canonicalizePath($to);
  349. $commonLength = 0;
  350. $minPathLength = min(strlen($from), strlen($to));
  351. // count how many chars the strings have in common
  352. for ($i = 0; $i < $minPathLength; $i++)
  353. {
  354. if ($from[$i] != $to[$i])
  355. {
  356. break;
  357. }
  358. if (DIRECTORY_SEPARATOR == $from[$i])
  359. {
  360. $commonLength = $i + 1;
  361. }
  362. }
  363. if ($commonLength)
  364. {
  365. $levelUp = substr_count($from, DIRECTORY_SEPARATOR, $commonLength);
  366. // up that many level
  367. $relativeDir = str_repeat('..'.DIRECTORY_SEPARATOR, $levelUp);
  368. // down the remaining $to path
  369. $relativeDir .= substr($to, $commonLength);
  370. return $relativeDir;
  371. }
  372. return $to;
  373. }
  374. /**
  375. * @param string A filesystem path
  376. *
  377. * @return string
  378. */
  379. protected function canonicalizePath($path)
  380. {
  381. if (empty($path))
  382. {
  383. return '';
  384. }
  385. $out = array();
  386. foreach (explode(DIRECTORY_SEPARATOR, $path) as $i => $fold)
  387. {
  388. if ('' == $fold || '.' == $fold)
  389. {
  390. continue;
  391. }
  392. if ('..' == $fold && $i > 0 && '..' != end($out))
  393. {
  394. array_pop($out);
  395. }
  396. else
  397. {
  398. $out[] = $fold;
  399. }
  400. }
  401. $result = DIRECTORY_SEPARATOR == $path[0] ? DIRECTORY_SEPARATOR : '';
  402. $result .= implode(DIRECTORY_SEPARATOR, $out);
  403. $result .= DIRECTORY_SEPARATOR == $path[strlen($path) - 1] ? DIRECTORY_SEPARATOR : '';
  404. return $result;
  405. }
  406. }

Debug toolbar