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