scratch – Blame information for rev
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
115 | office | 1 | <?php |
2 | |||
3 | /* |
||
4 | * This file is part of the Symfony package. |
||
5 | * |
||
6 | * (c) Fabien Potencier <fabien@symfony.com> |
||
7 | * |
||
8 | * For the full copyright and license information, please view the LICENSE |
||
9 | * file that was distributed with this source code. |
||
10 | */ |
||
11 | |||
12 | namespace Symfony\Component\Filesystem; |
||
13 | |||
14 | use Symfony\Component\Filesystem\Exception\IOException; |
||
15 | use Symfony\Component\Filesystem\Exception\FileNotFoundException; |
||
16 | |||
17 | /** |
||
18 | * Provides basic utility to manipulate the file system. |
||
19 | * |
||
20 | * @author Fabien Potencier <fabien@symfony.com> |
||
21 | */ |
||
22 | class Filesystem |
||
23 | { |
||
24 | /** |
||
25 | * Copies a file. |
||
26 | * |
||
27 | * If the target file is older than the origin file, it's always overwritten. |
||
28 | * If the target file is newer, it is overwritten only when the |
||
29 | * $overwriteNewerFiles option is set to true. |
||
30 | * |
||
31 | * @param string $originFile The original filename |
||
32 | * @param string $targetFile The target filename |
||
33 | * @param bool $overwriteNewerFiles If true, target files newer than origin files are overwritten |
||
34 | * |
||
35 | * @throws FileNotFoundException When originFile doesn't exist |
||
36 | * @throws IOException When copy fails |
||
37 | */ |
||
38 | public function copy($originFile, $targetFile, $overwriteNewerFiles = false) |
||
39 | { |
||
40 | if (stream_is_local($originFile) && !is_file($originFile)) { |
||
41 | throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); |
||
42 | } |
||
43 | |||
44 | $this->mkdir(dirname($targetFile)); |
||
45 | |||
46 | $doCopy = true; |
||
47 | if (!$overwriteNewerFiles && null === parse_url($originFile, PHP_URL_HOST) && is_file($targetFile)) { |
||
48 | $doCopy = filemtime($originFile) > filemtime($targetFile); |
||
49 | } |
||
50 | |||
51 | if ($doCopy) { |
||
52 | // https://bugs.php.net/bug.php?id=64634 |
||
53 | if (false === $source = @fopen($originFile, 'r')) { |
||
54 | throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading.', $originFile, $targetFile), 0, null, $originFile); |
||
55 | } |
||
56 | |||
57 | // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default |
||
58 | if (false === $target = @fopen($targetFile, 'w', null, stream_context_create(array('ftp' => array('overwrite' => true))))) { |
||
59 | throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing.', $originFile, $targetFile), 0, null, $originFile); |
||
60 | } |
||
61 | |||
62 | $bytesCopied = stream_copy_to_stream($source, $target); |
||
63 | fclose($source); |
||
64 | fclose($target); |
||
65 | unset($source, $target); |
||
66 | |||
67 | if (!is_file($targetFile)) { |
||
68 | throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); |
||
69 | } |
||
70 | |||
71 | // Like `cp`, preserve executable permission bits |
||
72 | @chmod($targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111)); |
||
73 | |||
74 | if (stream_is_local($originFile) && $bytesCopied !== ($bytesOrigin = filesize($originFile))) { |
||
75 | throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile); |
||
76 | } |
||
77 | } |
||
78 | } |
||
79 | |||
80 | /** |
||
81 | * Creates a directory recursively. |
||
82 | * |
||
83 | * @param string|array|\Traversable $dirs The directory path |
||
84 | * @param int $mode The directory mode |
||
85 | * |
||
86 | * @throws IOException On any directory creation failure |
||
87 | */ |
||
88 | public function mkdir($dirs, $mode = 0777) |
||
89 | { |
||
90 | foreach ($this->toIterator($dirs) as $dir) { |
||
91 | if (is_dir($dir)) { |
||
92 | continue; |
||
93 | } |
||
94 | |||
95 | if (true !== @mkdir($dir, $mode, true)) { |
||
96 | $error = error_get_last(); |
||
97 | if (!is_dir($dir)) { |
||
98 | // The directory was not created by a concurrent process. Let's throw an exception with a developer friendly error message if we have one |
||
99 | if ($error) { |
||
100 | throw new IOException(sprintf('Failed to create "%s": %s.', $dir, $error['message']), 0, null, $dir); |
||
101 | } |
||
102 | throw new IOException(sprintf('Failed to create "%s"', $dir), 0, null, $dir); |
||
103 | } |
||
104 | } |
||
105 | } |
||
106 | } |
||
107 | |||
108 | /** |
||
109 | * Checks the existence of files or directories. |
||
110 | * |
||
111 | * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to check |
||
112 | * |
||
113 | * @return bool true if the file exists, false otherwise |
||
114 | */ |
||
115 | public function exists($files) |
||
116 | { |
||
117 | foreach ($this->toIterator($files) as $file) { |
||
118 | if ('\\' === DIRECTORY_SEPARATOR && strlen($file) > 258) { |
||
119 | throw new IOException('Could not check if file exist because path length exceeds 258 characters.', 0, null, $file); |
||
120 | } |
||
121 | |||
122 | if (!file_exists($file)) { |
||
123 | return false; |
||
124 | } |
||
125 | } |
||
126 | |||
127 | return true; |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * Sets access and modification time of file. |
||
132 | * |
||
133 | * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to create |
||
134 | * @param int $time The touch time as a Unix timestamp |
||
135 | * @param int $atime The access time as a Unix timestamp |
||
136 | * |
||
137 | * @throws IOException When touch fails |
||
138 | */ |
||
139 | public function touch($files, $time = null, $atime = null) |
||
140 | { |
||
141 | foreach ($this->toIterator($files) as $file) { |
||
142 | $touch = $time ? @touch($file, $time, $atime) : @touch($file); |
||
143 | if (true !== $touch) { |
||
144 | throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file); |
||
145 | } |
||
146 | } |
||
147 | } |
||
148 | |||
149 | /** |
||
150 | * Removes files or directories. |
||
151 | * |
||
152 | * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to remove |
||
153 | * |
||
154 | * @throws IOException When removal fails |
||
155 | */ |
||
156 | public function remove($files) |
||
157 | { |
||
158 | if ($files instanceof \Traversable) { |
||
159 | $files = iterator_to_array($files, false); |
||
160 | } elseif (!is_array($files)) { |
||
161 | $files = array($files); |
||
162 | } |
||
163 | $files = array_reverse($files); |
||
164 | foreach ($files as $file) { |
||
165 | if (is_link($file)) { |
||
166 | // See https://bugs.php.net/52176 |
||
167 | if (!@(unlink($file) || '\\' !== DIRECTORY_SEPARATOR || rmdir($file)) && file_exists($file)) { |
||
168 | $error = error_get_last(); |
||
169 | throw new IOException(sprintf('Failed to remove symlink "%s": %s.', $file, $error['message'])); |
||
170 | } |
||
171 | } elseif (is_dir($file)) { |
||
172 | $this->remove(new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS)); |
||
173 | |||
174 | if (!@rmdir($file) && file_exists($file)) { |
||
175 | $error = error_get_last(); |
||
176 | throw new IOException(sprintf('Failed to remove directory "%s": %s.', $file, $error['message'])); |
||
177 | } |
||
178 | } elseif (!@unlink($file) && file_exists($file)) { |
||
179 | $error = error_get_last(); |
||
180 | throw new IOException(sprintf('Failed to remove file "%s": %s.', $file, $error['message'])); |
||
181 | } |
||
182 | } |
||
183 | } |
||
184 | |||
185 | /** |
||
186 | * Change mode for an array of files or directories. |
||
187 | * |
||
188 | * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change mode |
||
189 | * @param int $mode The new mode (octal) |
||
190 | * @param int $umask The mode mask (octal) |
||
191 | * @param bool $recursive Whether change the mod recursively or not |
||
192 | * |
||
193 | * @throws IOException When the change fail |
||
194 | */ |
||
195 | public function chmod($files, $mode, $umask = 0000, $recursive = false) |
||
196 | { |
||
197 | foreach ($this->toIterator($files) as $file) { |
||
198 | if (true !== @chmod($file, $mode & ~$umask)) { |
||
199 | throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file); |
||
200 | } |
||
201 | if ($recursive && is_dir($file) && !is_link($file)) { |
||
202 | $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); |
||
203 | } |
||
204 | } |
||
205 | } |
||
206 | |||
207 | /** |
||
208 | * Change the owner of an array of files or directories. |
||
209 | * |
||
210 | * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change owner |
||
211 | * @param string $user The new owner user name |
||
212 | * @param bool $recursive Whether change the owner recursively or not |
||
213 | * |
||
214 | * @throws IOException When the change fail |
||
215 | */ |
||
216 | public function chown($files, $user, $recursive = false) |
||
217 | { |
||
218 | foreach ($this->toIterator($files) as $file) { |
||
219 | if ($recursive && is_dir($file) && !is_link($file)) { |
||
220 | $this->chown(new \FilesystemIterator($file), $user, true); |
||
221 | } |
||
222 | if (is_link($file) && function_exists('lchown')) { |
||
223 | if (true !== @lchown($file, $user)) { |
||
224 | throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); |
||
225 | } |
||
226 | } else { |
||
227 | if (true !== @chown($file, $user)) { |
||
228 | throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); |
||
229 | } |
||
230 | } |
||
231 | } |
||
232 | } |
||
233 | |||
234 | /** |
||
235 | * Change the group of an array of files or directories. |
||
236 | * |
||
237 | * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change group |
||
238 | * @param string $group The group name |
||
239 | * @param bool $recursive Whether change the group recursively or not |
||
240 | * |
||
241 | * @throws IOException When the change fail |
||
242 | */ |
||
243 | public function chgrp($files, $group, $recursive = false) |
||
244 | { |
||
245 | foreach ($this->toIterator($files) as $file) { |
||
246 | if ($recursive && is_dir($file) && !is_link($file)) { |
||
247 | $this->chgrp(new \FilesystemIterator($file), $group, true); |
||
248 | } |
||
249 | if (is_link($file) && function_exists('lchgrp')) { |
||
250 | if (true !== @lchgrp($file, $group) || (defined('HHVM_VERSION') && !posix_getgrnam($group))) { |
||
251 | throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); |
||
252 | } |
||
253 | } else { |
||
254 | if (true !== @chgrp($file, $group)) { |
||
255 | throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); |
||
256 | } |
||
257 | } |
||
258 | } |
||
259 | } |
||
260 | |||
261 | /** |
||
262 | * Renames a file or a directory. |
||
263 | * |
||
264 | * @param string $origin The origin filename or directory |
||
265 | * @param string $target The new filename or directory |
||
266 | * @param bool $overwrite Whether to overwrite the target if it already exists |
||
267 | * |
||
268 | * @throws IOException When target file or directory already exists |
||
269 | * @throws IOException When origin cannot be renamed |
||
270 | */ |
||
271 | public function rename($origin, $target, $overwrite = false) |
||
272 | { |
||
273 | // we check that target does not exist |
||
274 | if (!$overwrite && $this->isReadable($target)) { |
||
275 | throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); |
||
276 | } |
||
277 | |||
278 | if (true !== @rename($origin, $target)) { |
||
117 | office | 279 | if (is_dir($origin)) { |
280 | // See https://bugs.php.net/bug.php?id=54097 & http://php.net/manual/en/function.rename.php#113943 |
||
281 | $this->mirror($origin, $target, null, array('override' => $overwrite, 'delete' => $overwrite)); |
||
282 | $this->remove($origin); |
||
283 | |||
284 | return; |
||
285 | } |
||
115 | office | 286 | throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target); |
287 | } |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * Tells whether a file exists and is readable. |
||
292 | * |
||
293 | * @param string $filename Path to the file |
||
294 | * |
||
295 | * @return bool |
||
296 | * |
||
297 | * @throws IOException When windows path is longer than 258 characters |
||
298 | */ |
||
299 | private function isReadable($filename) |
||
300 | { |
||
301 | if ('\\' === DIRECTORY_SEPARATOR && strlen($filename) > 258) { |
||
302 | throw new IOException('Could not check if file is readable because path length exceeds 258 characters.', 0, null, $filename); |
||
303 | } |
||
304 | |||
305 | return is_readable($filename); |
||
306 | } |
||
307 | |||
308 | /** |
||
309 | * Creates a symbolic link or copy a directory. |
||
310 | * |
||
311 | * @param string $originDir The origin directory path |
||
312 | * @param string $targetDir The symbolic link name |
||
313 | * @param bool $copyOnWindows Whether to copy files if on Windows |
||
314 | * |
||
315 | * @throws IOException When symlink fails |
||
316 | */ |
||
317 | public function symlink($originDir, $targetDir, $copyOnWindows = false) |
||
318 | { |
||
319 | if ('\\' === DIRECTORY_SEPARATOR) { |
||
320 | $originDir = strtr($originDir, '/', '\\'); |
||
321 | $targetDir = strtr($targetDir, '/', '\\'); |
||
322 | |||
323 | if ($copyOnWindows) { |
||
324 | $this->mirror($originDir, $targetDir); |
||
325 | |||
326 | return; |
||
327 | } |
||
328 | } |
||
329 | |||
330 | $this->mkdir(dirname($targetDir)); |
||
331 | |||
332 | $ok = false; |
||
333 | if (is_link($targetDir)) { |
||
334 | if (readlink($targetDir) != $originDir) { |
||
335 | $this->remove($targetDir); |
||
336 | } else { |
||
337 | $ok = true; |
||
338 | } |
||
339 | } |
||
340 | |||
341 | if (!$ok && true !== @symlink($originDir, $targetDir)) { |
||
342 | $this->linkException($originDir, $targetDir, 'symbolic'); |
||
343 | } |
||
344 | } |
||
345 | |||
346 | /** |
||
347 | * Creates a hard link, or several hard links to a file. |
||
348 | * |
||
349 | * @param string $originFile The original file |
||
350 | * @param string|string[] $targetFiles The target file(s) |
||
351 | * |
||
352 | * @throws FileNotFoundException When original file is missing or not a file |
||
353 | * @throws IOException When link fails, including if link already exists |
||
354 | */ |
||
355 | public function hardlink($originFile, $targetFiles) |
||
356 | { |
||
357 | if (!$this->exists($originFile)) { |
||
358 | throw new FileNotFoundException(null, 0, null, $originFile); |
||
359 | } |
||
360 | |||
361 | if (!is_file($originFile)) { |
||
362 | throw new FileNotFoundException(sprintf('Origin file "%s" is not a file', $originFile)); |
||
363 | } |
||
364 | |||
365 | foreach ($this->toIterator($targetFiles) as $targetFile) { |
||
366 | if (is_file($targetFile)) { |
||
367 | if (fileinode($originFile) === fileinode($targetFile)) { |
||
368 | continue; |
||
369 | } |
||
370 | $this->remove($targetFile); |
||
371 | } |
||
372 | |||
373 | if (true !== @link($originFile, $targetFile)) { |
||
374 | $this->linkException($originFile, $targetFile, 'hard'); |
||
375 | } |
||
376 | } |
||
377 | } |
||
378 | |||
379 | /** |
||
380 | * @param string $origin |
||
381 | * @param string $target |
||
382 | * @param string $linkType Name of the link type, typically 'symbolic' or 'hard' |
||
383 | */ |
||
384 | private function linkException($origin, $target, $linkType) |
||
385 | { |
||
386 | $report = error_get_last(); |
||
387 | if (is_array($report)) { |
||
388 | if ('\\' === DIRECTORY_SEPARATOR && false !== strpos($report['message'], 'error code(1314)')) { |
||
389 | throw new IOException(sprintf('Unable to create %s link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target); |
||
390 | } |
||
391 | } |
||
392 | throw new IOException(sprintf('Failed to create %s link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target); |
||
393 | } |
||
394 | |||
395 | /** |
||
396 | * Resolves links in paths. |
||
397 | * |
||
398 | * With $canonicalize = false (default) |
||
399 | * - if $path does not exist or is not a link, returns null |
||
400 | * - if $path is a link, returns the next direct target of the link without considering the existence of the target |
||
401 | * |
||
402 | * With $canonicalize = true |
||
403 | * - if $path does not exist, returns null |
||
404 | * - if $path exists, returns its absolute fully resolved final version |
||
405 | * |
||
406 | * @param string $path A filesystem path |
||
407 | * @param bool $canonicalize Whether or not to return a canonicalized path |
||
408 | * |
||
409 | * @return string|null |
||
410 | */ |
||
411 | public function readlink($path, $canonicalize = false) |
||
412 | { |
||
413 | if (!$canonicalize && !is_link($path)) { |
||
414 | return; |
||
415 | } |
||
416 | |||
417 | if ($canonicalize) { |
||
418 | if (!$this->exists($path)) { |
||
419 | return; |
||
420 | } |
||
421 | |||
422 | if ('\\' === DIRECTORY_SEPARATOR) { |
||
423 | $path = readlink($path); |
||
424 | } |
||
425 | |||
426 | return realpath($path); |
||
427 | } |
||
428 | |||
429 | if ('\\' === DIRECTORY_SEPARATOR) { |
||
430 | return realpath($path); |
||
431 | } |
||
432 | |||
433 | return readlink($path); |
||
434 | } |
||
435 | |||
436 | /** |
||
437 | * Given an existing path, convert it to a path relative to a given starting path. |
||
438 | * |
||
439 | * @param string $endPath Absolute path of target |
||
440 | * @param string $startPath Absolute path where traversal begins |
||
441 | * |
||
442 | * @return string Path of target relative to starting path |
||
443 | */ |
||
444 | public function makePathRelative($endPath, $startPath) |
||
445 | { |
||
446 | // Normalize separators on Windows |
||
447 | if ('\\' === DIRECTORY_SEPARATOR) { |
||
448 | $endPath = str_replace('\\', '/', $endPath); |
||
449 | $startPath = str_replace('\\', '/', $startPath); |
||
450 | } |
||
451 | |||
452 | // Split the paths into arrays |
||
453 | $startPathArr = explode('/', trim($startPath, '/')); |
||
454 | $endPathArr = explode('/', trim($endPath, '/')); |
||
455 | |||
456 | if ('/' !== $startPath[0]) { |
||
457 | array_shift($startPathArr); |
||
458 | } |
||
459 | |||
460 | if ('/' !== $endPath[0]) { |
||
461 | array_shift($endPathArr); |
||
462 | } |
||
463 | |||
464 | $normalizePathArray = function ($pathSegments) { |
||
465 | $result = array(); |
||
466 | |||
467 | foreach ($pathSegments as $segment) { |
||
468 | if ('..' === $segment) { |
||
469 | array_pop($result); |
||
470 | } else { |
||
471 | $result[] = $segment; |
||
472 | } |
||
473 | } |
||
474 | |||
475 | return $result; |
||
476 | }; |
||
477 | |||
478 | $startPathArr = $normalizePathArray($startPathArr); |
||
479 | $endPathArr = $normalizePathArray($endPathArr); |
||
480 | |||
481 | // Find for which directory the common path stops |
||
482 | $index = 0; |
||
483 | while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) { |
||
484 | ++$index; |
||
485 | } |
||
486 | |||
487 | // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels) |
||
488 | if (count($startPathArr) === 1 && $startPathArr[0] === '') { |
||
489 | $depth = 0; |
||
490 | } else { |
||
491 | $depth = count($startPathArr) - $index; |
||
492 | } |
||
493 | |||
494 | // When we need to traverse from the start, and we are starting from a root path, don't add '../' |
||
495 | if ('/' === $startPath[0] && 0 === $index && 0 === $depth) { |
||
496 | $traverser = ''; |
||
497 | } else { |
||
498 | // Repeated "../" for each level need to reach the common path |
||
499 | $traverser = str_repeat('../', $depth); |
||
500 | } |
||
501 | |||
502 | $endPathRemainder = implode('/', array_slice($endPathArr, $index)); |
||
503 | |||
504 | // Construct $endPath from traversing to the common path, then to the remaining $endPath |
||
505 | $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : ''); |
||
506 | |||
507 | return '' === $relativePath ? './' : $relativePath; |
||
508 | } |
||
509 | |||
510 | /** |
||
511 | * Mirrors a directory to another. |
||
512 | * |
||
513 | * @param string $originDir The origin directory |
||
514 | * @param string $targetDir The target directory |
||
515 | * @param \Traversable $iterator A Traversable instance |
||
516 | * @param array $options An array of boolean options |
||
517 | * Valid options are: |
||
518 | * - $options['override'] Whether to override an existing file on copy or not (see copy()) |
||
519 | * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink()) |
||
520 | * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false) |
||
521 | * |
||
522 | * @throws IOException When file type is unknown |
||
523 | */ |
||
524 | public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array()) |
||
525 | { |
||
526 | $targetDir = rtrim($targetDir, '/\\'); |
||
527 | $originDir = rtrim($originDir, '/\\'); |
||
528 | |||
529 | // Iterate in destination folder to remove obsolete entries |
||
530 | if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) { |
||
531 | $deleteIterator = $iterator; |
||
532 | if (null === $deleteIterator) { |
||
533 | $flags = \FilesystemIterator::SKIP_DOTS; |
||
534 | $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST); |
||
535 | } |
||
536 | foreach ($deleteIterator as $file) { |
||
537 | $origin = str_replace($targetDir, $originDir, $file->getPathname()); |
||
538 | if (!$this->exists($origin)) { |
||
539 | $this->remove($file); |
||
540 | } |
||
541 | } |
||
542 | } |
||
543 | |||
544 | $copyOnWindows = false; |
||
545 | if (isset($options['copy_on_windows'])) { |
||
546 | $copyOnWindows = $options['copy_on_windows']; |
||
547 | } |
||
548 | |||
549 | if (null === $iterator) { |
||
550 | $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS; |
||
551 | $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST); |
||
552 | } |
||
553 | |||
554 | if ($this->exists($originDir)) { |
||
555 | $this->mkdir($targetDir); |
||
556 | } |
||
557 | |||
558 | foreach ($iterator as $file) { |
||
559 | $target = str_replace($originDir, $targetDir, $file->getPathname()); |
||
560 | |||
561 | if ($copyOnWindows) { |
||
562 | if (is_file($file)) { |
||
563 | $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); |
||
564 | } elseif (is_dir($file)) { |
||
565 | $this->mkdir($target); |
||
566 | } else { |
||
567 | throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); |
||
568 | } |
||
569 | } else { |
||
570 | if (is_link($file)) { |
||
571 | $this->symlink($file->getLinkTarget(), $target); |
||
572 | } elseif (is_dir($file)) { |
||
573 | $this->mkdir($target); |
||
574 | } elseif (is_file($file)) { |
||
575 | $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); |
||
576 | } else { |
||
577 | throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); |
||
578 | } |
||
579 | } |
||
580 | } |
||
581 | } |
||
582 | |||
583 | /** |
||
584 | * Returns whether the file path is an absolute path. |
||
585 | * |
||
586 | * @param string $file A file path |
||
587 | * |
||
588 | * @return bool |
||
589 | */ |
||
590 | public function isAbsolutePath($file) |
||
591 | { |
||
592 | return strspn($file, '/\\', 0, 1) |
||
593 | || (strlen($file) > 3 && ctype_alpha($file[0]) |
||
594 | && substr($file, 1, 1) === ':' |
||
595 | && strspn($file, '/\\', 2, 1) |
||
596 | ) |
||
597 | || null !== parse_url($file, PHP_URL_SCHEME) |
||
598 | ; |
||
599 | } |
||
600 | |||
601 | /** |
||
602 | * Creates a temporary file with support for custom stream wrappers. |
||
603 | * |
||
604 | * @param string $dir The directory where the temporary filename will be created |
||
605 | * @param string $prefix The prefix of the generated temporary filename |
||
606 | * Note: Windows uses only the first three characters of prefix |
||
607 | * |
||
608 | * @return string The new temporary filename (with path), or throw an exception on failure |
||
609 | */ |
||
610 | public function tempnam($dir, $prefix) |
||
611 | { |
||
612 | list($scheme, $hierarchy) = $this->getSchemeAndHierarchy($dir); |
||
613 | |||
614 | // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem |
||
615 | if (null === $scheme || 'file' === $scheme || 'gs' === $scheme) { |
||
616 | $tmpFile = @tempnam($hierarchy, $prefix); |
||
617 | |||
618 | // If tempnam failed or no scheme return the filename otherwise prepend the scheme |
||
619 | if (false !== $tmpFile) { |
||
620 | if (null !== $scheme && 'gs' !== $scheme) { |
||
621 | return $scheme.'://'.$tmpFile; |
||
622 | } |
||
623 | |||
624 | return $tmpFile; |
||
625 | } |
||
626 | |||
627 | throw new IOException('A temporary file could not be created.'); |
||
628 | } |
||
629 | |||
630 | // Loop until we create a valid temp file or have reached 10 attempts |
||
631 | for ($i = 0; $i < 10; ++$i) { |
||
632 | // Create a unique filename |
||
633 | $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true); |
||
634 | |||
635 | // Use fopen instead of file_exists as some streams do not support stat |
||
636 | // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability |
||
637 | $handle = @fopen($tmpFile, 'x+'); |
||
638 | |||
639 | // If unsuccessful restart the loop |
||
640 | if (false === $handle) { |
||
641 | continue; |
||
642 | } |
||
643 | |||
644 | // Close the file if it was successfully opened |
||
645 | @fclose($handle); |
||
646 | |||
647 | return $tmpFile; |
||
648 | } |
||
649 | |||
650 | throw new IOException('A temporary file could not be created.'); |
||
651 | } |
||
652 | |||
653 | /** |
||
654 | * Atomically dumps content into a file. |
||
655 | * |
||
656 | * @param string $filename The file to be written to |
||
657 | * @param string $content The data to write into the file |
||
658 | * |
||
659 | * @throws IOException If the file cannot be written to |
||
660 | */ |
||
661 | public function dumpFile($filename, $content) |
||
662 | { |
||
663 | $dir = dirname($filename); |
||
664 | |||
665 | if (!is_dir($dir)) { |
||
666 | $this->mkdir($dir); |
||
667 | } |
||
668 | |||
669 | if (!is_writable($dir)) { |
||
670 | throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); |
||
671 | } |
||
672 | |||
673 | // Will create a temp file with 0600 access rights |
||
674 | // when the filesystem supports chmod. |
||
675 | $tmpFile = $this->tempnam($dir, basename($filename)); |
||
676 | |||
677 | if (false === @file_put_contents($tmpFile, $content)) { |
||
678 | throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename); |
||
679 | } |
||
680 | |||
681 | @chmod($tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask()); |
||
682 | |||
683 | $this->rename($tmpFile, $filename, true); |
||
684 | } |
||
685 | |||
686 | /** |
||
687 | * Appends content to an existing file. |
||
688 | * |
||
689 | * @param string $filename The file to which to append content |
||
690 | * @param string $content The content to append |
||
691 | * |
||
692 | * @throws IOException If the file is not writable |
||
693 | */ |
||
694 | public function appendToFile($filename, $content) |
||
695 | { |
||
696 | $dir = dirname($filename); |
||
697 | |||
698 | if (!is_dir($dir)) { |
||
699 | $this->mkdir($dir); |
||
700 | } |
||
701 | |||
702 | if (!is_writable($dir)) { |
||
703 | throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); |
||
704 | } |
||
705 | |||
706 | if (false === @file_put_contents($filename, $content, FILE_APPEND)) { |
||
707 | throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename); |
||
708 | } |
||
709 | } |
||
710 | |||
711 | /** |
||
712 | * @param mixed $files |
||
713 | * |
||
714 | * @return \Traversable |
||
715 | */ |
||
716 | private function toIterator($files) |
||
717 | { |
||
718 | if (!$files instanceof \Traversable) { |
||
719 | $files = new \ArrayObject(is_array($files) ? $files : array($files)); |
||
720 | } |
||
721 | |||
722 | return $files; |
||
723 | } |
||
724 | |||
725 | /** |
||
726 | * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> array(file, tmp)). |
||
727 | * |
||
728 | * @param string $filename The filename to be parsed |
||
729 | * |
||
730 | * @return array The filename scheme and hierarchical part |
||
731 | */ |
||
732 | private function getSchemeAndHierarchy($filename) |
||
733 | { |
||
734 | $components = explode('://', $filename, 2); |
||
735 | |||
736 | return 2 === count($components) ? array($components[0], $components[1]) : array(null, $components[0]); |
||
737 | } |
||
738 | } |