| 9 | | |
| 10 | | /** |
| 11 | | * |
| 12 | | * Returns a data folder in which files can be read and written by |
| 13 | | * the web server, but NOT seen as part of the server's document space. |
| 14 | | * Automatically checks for overriding path settings via app.yml so |
| 15 | | * you can customize these directory settings. |
| 16 | | * getWritableDataFolder() returns sf_data_dir/a_writable unless |
| 17 | | * overridden by app_aToolkit_writable_dir. Note that this main directory |
| 18 | | * is automatically chmodded appropriately by symfony project:permissions. |
| 19 | | * (apostrophePlugin registers an event handler that extends this task.) |
| 20 | | * getWritableDataFolder(array('indexes')) returns |
| 21 | | * sf_data_dir/a_writable/indexes unless overridden by |
| 22 | | * app_aToolkit_writable_indexes_dir (first preference) or |
| 23 | | * app_aToolkit_writable_dir (second preference). If app_aToolkit_writable_indexes_dir |
| 24 | | * is not set, but app_aToolkit_writable_dir is found, then |
| 25 | | * /indexes will be appended to app_aToolkit_writable_dir. |
| 26 | | * You may supply more than one component in the array. For instance, |
| 27 | | * getWritableDataFolder(array('indexes', 'purple')) returns |
| 28 | | * sf_data_dir/a_writable/indexes/purple unless overridden by |
| 29 | | * app_aToolkit_writable_indexes_purple_dir (first choice), or |
| 30 | | * app_aToolkit_writable_indexes_dir (second choice), or |
| 31 | | * app_aToolkit_writable_dir (third choice). |
| 32 | | * You can also pass a single path argument rather than an |
| 33 | | * array, in which case it is split into components at the slashes, |
| 34 | | * with any leading and trailing slashes removed first. |
| 35 | | * Always attempts to create the folder if needed. This generally |
| 36 | | * succeeds except for the top level sf_data_dir/a_writable folder, |
| 37 | | * so you'll need to create that folder and make it readable, |
| 38 | | * writable and executable by the web server (chmod 777 in many cases). |
| 39 | | * Occurrences of SF_DATA_DIR in the final path will be automatically |
| 40 | | * replaced with the value of sfConfig::get('sf_data_dir'). This is |
| 41 | | * useful when specifying alternate paths in app.yml, e.g. |
| 42 | | * (to be compatible with a very early release of our CMS): |
| 43 | | * a_writable_zend_indexes: SF_DATA_DIR/zendIndexes |
| 44 | | * SF_WEB_DIR is supported in the same way. |
| 45 | | * @param mixed $components |
| 46 | | * @return mixed |
| 47 | | */ |
| 48 | | static public function getWritableDataFolder($components = array()) |
| 49 | | { |
| 50 | | return self::getOrCreateFolder("app_aToolkit_writable_dir", |
| 51 | | sfConfig::get('sf_data_dir') . DIRECTORY_SEPARATOR . 'a_writable', |
| 52 | | $components); |
| 53 | | } |
| 54 | | |
| 55 | | /** |
| 56 | | * |
| 57 | | * Returns a subfolder of the project's upload folder in which files |
| 58 | | * can be read and written by the web server and also seen as part of the |
| 59 | | * web server's document space. Automatically checks for overriding |
| 60 | | * path settings via app.yml so you can customize these directory settings. |
| 61 | | * getUploadFolder() returns sf_upload_dir unless |
| 62 | | * overridden by app_aToolkit_upload_dir. |
| 63 | | * getUploadFolder(array('media')) returns sf_upload_dir/media |
| 64 | | * unless overridden by app_aToolkit_upload_media_dir (first preference) or |
| 65 | | * app_aToolkit_upload_dir (second preference). If app_aToolkit_upload_media_dir |
| 66 | | * is not set, but app_aToolkit_upload_dir is found, then |
| 67 | | * /media will be appended to app_aToolkit_upload_dir. |
| 68 | | * You may supply more than one component in the array. For instance, |
| 69 | | * getUploadFolder(array('media', 'jpegs')) returns |
| 70 | | * sf_upload_dir/media/jpegs unless overridden by |
| 71 | | * app_aToolkit_upload_media_jpegs_dir (first choice), or |
| 72 | | * app_aToolkit_upload_media_dir (second choice), or |
| 73 | | * app_aToolkit_upload_dir (third choice). |
| 74 | | * You can also pass a single path argument rather than an |
| 75 | | * array, in which case it is split into components at the slashes, |
| 76 | | * with any leading and trailing slashes removed first. |
| 77 | | * Always attempts to create the folder if needed. This generally |
| 78 | | * succeeds because Symfony projects have a world-writable |
| 79 | | * top-level web/upload folder by default. |
| 80 | | * Occurrences of SF_DATA_DIR in the final path will be automatically |
| 81 | | * replaced with the value of sfConfig::get('sf_data_dir'). This is |
| 82 | | * useful when specifying alternate paths in app.yml, e.g. |
| 83 | | * (to be compatible with a very early release of our CMS): |
| 84 | | * a_writable_zend_indexes: SF_DATA_DIR/zendIndexes |
| 85 | | * SF_WEB_DIR is supported in the same way. |
| 86 | | * @param mixed $components |
| 87 | | * @return mixed |
| 88 | | */ |
| 89 | | static public function getUploadFolder($components = array()) |
| 90 | | { |
| 91 | | return self::getOrCreateFolder("app_aToolkit_upload_dir", |
| 92 | | sfConfig::get('sf_upload_dir'), $components); |
| 93 | | } |
| 94 | | |
| 95 | | static protected $folderCache = array(); |
| 96 | | |
| 97 | | /** |
| 98 | | * |
| 99 | | * Returns a subfolder of $basePath. |
| 100 | | * Automatically checks for overriding path settings via app.yml |
| 101 | | * so you can customize these directory settings. |
| 102 | | * getOrCreateFolder('app_key_dir', '/path') returns /path unless |
| 103 | | * overridden by the Symfony config setting app_key_dir. |
| 104 | | * getOrCreateFolder('app_key_dir', '/path', array('media')) returns |
| 105 | | * /path/media unless overridden by app_key_media_dir (first preference) or |
| 106 | | * app_key_dir (second preference). If app_key_media_dir |
| 107 | | * is not set, but app_key_dir is set, then |
| 108 | | * /media will be appended to app_key_dir. |
| 109 | | * You may supply more than one component in the array. For instance, |
| 110 | | * getOrCreateFolder('app_key_dir', '/path', array('media', 'jpegs')) |
| 111 | | * returns /path/media/jpegs unless overridden by |
| 112 | | * app_key_media_jpegs_dir (first choice), or |
| 113 | | * app_key_media_dir (second choice), or |
| 114 | | * app_key_dir (third choice). |
| 115 | | * You can also pass a single path argument rather than an |
| 116 | | * array, in which case it is split into components at the slashes, |
| 117 | | * with any leading and trailing slashes removed first. |
| 118 | | * Always attempts to create the folder if needed. This generally |
| 119 | | * succeeds because Symfony projects have a world-writable |
| 120 | | * top-level web/upload folder by default. |
| 121 | | * Occurrences of SF_DATA_DIR in the final path will be automatically |
| 122 | | * replaced with the value of sfConfig::get('sf_data_dir'). This is |
| 123 | | * useful when specifying alternate paths in app.yml, e.g. |
| 124 | | * (to be compatible with a very early release of our CMS): |
| 125 | | * all: |
| 126 | | * aToolkit: |
| 127 | | * _writable_zend_indexes_dir: SF_DATA_DIR/zendIndexes |
| 128 | | * SF_WEB_DIR is supported in the same way. |
| 129 | | * |
| 130 | | * Results of this call are cached for the duration of the request so that you can |
| 131 | | * call it repeatedly without hitting the filesystem with slow stat() calls. |
| 132 | | * |
| 133 | | * @param mixed $baseKey |
| 134 | | * @param mixed $basePath |
| 135 | | * @param mixed $components |
| 136 | | * @return mixed |
| 137 | | */ |
| 138 | | static public function getOrCreateFolder($baseKey, $basePath, $components = array()) |
| 139 | | { |
| 140 | | if (!is_array($components)) |
| 141 | | { |
| 142 | | $components = preg_split("/\//", $components, -1, PREG_SPLIT_NO_EMPTY); |
| 143 | | } |
| 144 | | $cacheKey = implode('/', $components); |
| 145 | | if (strlen($cacheKey)) |
| 146 | | { |
| 147 | | $cacheKey = $baseKey . $cacheKey; |
| 148 | | } |
| 149 | | else |
| 150 | | { |
| 151 | | $cacheKey = $baseKey; |
| 152 | | } |
| 153 | | // Keep trying to find it in the per-request cache, first by |
| 154 | | // checking the persistent cache, then by actually doing the |
| 155 | | // slow filesystem stat() and mkdir() work |
| 156 | | if (!isset(aFiles::$folderCache[$cacheKey])) |
| 157 | | { |
| 158 | | $persistentCache = aCacheTools::get('folder'); |
| 159 | | aFiles::$folderCache[$cacheKey] = $persistentCache->get($cacheKey); |
| 160 | | } |
| 161 | | if (!isset(aFiles::$folderCache[$cacheKey])) |
| 162 | | { |
| 163 | | $key = $baseKey; |
| 164 | | $count = count($components); |
| 165 | | $path = false; |
| 166 | | $baseKeyStem = $baseKey; |
| 167 | | $pos = strpos($baseKey, "_dir"); |
| 168 | | if ($pos !== false) |
| 169 | | { |
| 170 | | $baseKeyStem = substr($baseKey, 0, $pos) . "_"; |
| 171 | | } |
| 172 | | for ($i = $count; ($i >= 0); $i--) |
| 173 | | { |
| 174 | | if ($i === 0) |
| 175 | | { |
| 176 | | $key = $baseKey; |
| 177 | | } |
| 178 | | else |
| 179 | | { |
| 180 | | $key = $baseKeyStem . |
| 181 | | implode("_", array_slice($components, 0, $i)) . "_dir"; |
| 182 | | } |
| 183 | | $default = false; |
| 184 | | if ($i === 0) |
| 185 | | { |
| 186 | | $default = $basePath; |
| 187 | | } |
| 188 | | $result = sfConfig::get($key, $default); |
| 189 | | if ($result !== false) |
| 190 | | { |
| 191 | | $remainder = implode(DIRECTORY_SEPARATOR, array_slice($components, $i)); |
| 192 | | $ancestor = $result; |
| 193 | | if (strlen($remainder)) |
| 194 | | { |
| 195 | | $path = $result . DIRECTORY_SEPARATOR . $remainder; |
| 196 | | } |
| 197 | | else |
| 198 | | { |
| 199 | | $path = $result; |
| 200 | | } |
| 201 | | break; |
| 202 | | } |
| 203 | | } |
| 204 | | |
| 205 | | $path = str_replace( |
| 206 | | array("SF_DATA_DIR", "SF_WEB_DIR"), |
| 207 | | array(sfConfig::get('sf_data_dir'), sfConfig::get('sf_web_dir')), |
| 208 | | $path); |
| 209 | | if (!is_dir($path)) |
| 210 | | { |
| 211 | | // There's a recursive mkdir flag in PHP 5.x, neato |
| 212 | | if (!mkdir($path, 0777, true)) |
| 213 | | { |
| 214 | | // It's better to report $ancestor rather than $path because |
| 215 | | // creating that one parent should solve the problem |
| 216 | | throw new Exception("Unable to create $path in $ancestor the admin will probably need to do this manually the first time and set permissions so that the web server can write to that folder"); |
| 217 | | } |
| 218 | | } |
| 219 | | aFiles::$folderCache[$cacheKey] = $path; |
| 220 | | $persistentCache->set($cacheKey, $path, 86400 * 365); |
| 221 | | } |
| 222 | | return aFiles::$folderCache[$cacheKey]; |
| 223 | | } |
| 224 | | |
| 225 | | /** |
| 226 | | * |
| 227 | | * Symfony has a getTempDir method in sfToolkit but it is only |
| 228 | | * used by unit tests. It relies on the system temporary folder |
| 229 | | * which might not always be accessible in a non-command-line |
| 230 | | * PHP environment. Let's use something more local to our project. |
| 231 | | * @return mixed |
| 232 | | */ |
| 233 | | static public function getTemporaryFileFolder() |
| 234 | | { |
| 235 | | return self::getWritableDataFolder(array("tmp")); |
| 236 | | } |
| 237 | | |
| 238 | | /** |
| 239 | | * DOCUMENT ME |
| 240 | | * @return mixed |
| 241 | | */ |
| 242 | | static public function getTemporaryFilename() |
| 243 | | { |
| 244 | | |
| 245 | | $filename = aGuid::generate(); |
| 246 | | $tempDir = self::getTemporaryFileFolder(); |
| 247 | | return $tempDir . DIRECTORY_SEPARATOR . $filename; |
| 248 | | } |
| 249 | | |
| 250 | | static public function touch($file) |
| 251 | | { |
| 252 | | // Update the modification time of the file, even if it |
| 253 | | // is accessed via a stream wrapper. PHP does not support |
| 254 | | // this otherwise in the regular touch() function |
| 255 | | $out = fopen($file, "a"); |
| 256 | | if ($out) |
| 257 | | { |
| 258 | | fclose($out); |
| 259 | | return true; |
| 260 | | } |
| 261 | | return false; |
| 262 | | } |
| 263 | | |
| 264 | | /** |
| 265 | | * Returns array of filenames in directory, without the useless and dangerous . and .. entries, |
| 266 | | * using only functions that stream wrappers support. Returns just the basenames, the |
| 267 | | * full path is NOT returned unless you specify $options['fullPath'] = true. Returns false |
| 268 | | * if the path does not exist or is not a directory (you may get an empty list for stream wrappers |
| 269 | | * that can't really make this distinction) |
| 270 | | */ |
| 271 | | static public function ls($path, $options = array()) |
| 272 | | { |
| 273 | | $dir = @opendir($path); |
| 274 | | if ($dir === false) |
| 275 | | { |
| 276 | | return false; |
| 277 | | } |
| 278 | | $files = array(); |
| 279 | | $fullPath = isset($options['fullPath']) && $options['fullPath']; |
| 280 | | if ($fullPath) |
| 281 | | { |
| 282 | | $prependPath = preg_replace('/\/$/', '', $path); |
| 283 | | } |
| 284 | | while (($file = readdir($dir)) !== false) |
| 285 | | { |
| 286 | | if (($file === '.') || ($file === '..')) |
| 287 | | { |
| 288 | | continue; |
| 289 | | } |
| 290 | | if ($fullPath) |
| 291 | | { |
| 292 | | $files[] = "$prependPath/$file"; |
| 293 | | } |
| 294 | | else |
| 295 | | { |
| 296 | | $files[] = $file; |
| 297 | | } |
| 298 | | } |
| 299 | | closedir($dir); |
| 300 | | return $files; |
| 301 | | } |
| 302 | | |
| 303 | | /** |
| 304 | | * A partial implementation of glob() that uses only functions that stream wrappers support. |
| 305 | | * Right now this implementation is very limited: you can only have one * wildcard and it must |
| 306 | | * be in the last component of the path. The . and .. entries are never returned. Subdirectories |
| 307 | | * are returned. Performance would be better if we used native globbing functionality of the |
| 308 | | * underlying implementations like S3 to avoid pulling a list of everything in the folder first. |
| 309 | | * For now it's good enough for the media repository's needs |
| 310 | | * |
| 311 | | * Returns a full path, because glob does |
| 312 | | */ |
| 313 | | |
| 314 | | static public function glob($pattern) |
| 315 | | { |
| 316 | | $path = dirname($pattern); |
| 317 | | $pattern = basename($pattern); |
| 318 | | $pattern = '/^' . str_replace('\*', '.*', preg_quote($pattern, '/')) . '$/'; |
| 319 | | $files = aFiles::ls($path); |
| 320 | | $results = array(); |
| 321 | | foreach ($files as $file) |
| 322 | | { |
| 323 | | if (preg_match($pattern, $file)) |
| 324 | | { |
| 325 | | $results[] = $path . '/' . $file; |
| 326 | | } |
| 327 | | } |
| 328 | | return $results; |
| 329 | | } |
| 330 | | |
| 331 | | /** |
| 332 | | * $statInfo is an array returned by stat(). Determines whether |
| 333 | | * it ultimately refers to a regular file |
| 334 | | */ |
| 335 | | static public function statIsFile($statInfo) |
| 336 | | { |
| 337 | | return $statInfo['mode'] & 0100000; |
| 338 | | } |
| 339 | | |
| 340 | | /** |
| 341 | | * $statInfo() is an array returned by stat(). Determines whether |
| 342 | | * it ultimately refers to a directory |
| 343 | | */ |
| 344 | | static public function statIsDir($statInfo) |
| 345 | | { |
| 346 | | return $statInfo['mode'] & 0040000; |
| 347 | | } |
| 348 | | |
| 349 | | /** |
| 350 | | * Be careful with this, it follows symlinks if any. Mainly for stream wrappers |
| 351 | | */ |
| 352 | | static public function rmRf($path) |
| 353 | | { |
| 354 | | $stat = @stat($path); |
| 355 | | if (!$stat) |
| 356 | | { |
| 357 | | return; |
| 358 | | } |
| 359 | | if (aFiles::statIsDir($stat)) |
| 360 | | { |
| 361 | | $list = aFiles::ls($path); |
| 362 | | foreach ($list as $file) |
| 363 | | { |
| 364 | | $filePath = "$path/$file"; |
| 365 | | if (strlen($filePath) < strlen($path)) |
| 366 | | { |
| 367 | | throw new sfException("I almost tried to delete something higher up the original, I don't like this, bailing out"); |
| 368 | | } |
| 369 | | aFiles::rmRf($filePath); |
| 370 | | } |
| 371 | | if (!rmdir($path)) |
| 372 | | { |
| 373 | | return false; |
| 374 | | } |
| 375 | | } |
| 376 | | else |
| 377 | | { |
| 378 | | if (!unlink($path)) |
| 379 | | { |
| 380 | | return false; |
| 381 | | } |
| 382 | | } |
| 383 | | return true; |
| 384 | | } |
| 385 | | |
| 386 | | /** |
| 387 | | * Make one directory a mirror of the other, deleting and adding files as needed. |
| 388 | | * Creates $to if necessary. Source files that disappear in mid-sync log a warning. |
| 389 | | * Failures to write to the destination are considered serious errors and result in the |
| 390 | | * entire operation returning false. |
| 391 | | * |
| 392 | | * Compares sizes and modification dates to determine whether source is newer |
| 393 | | * than destination unless 'force' => true is specified as an option. In addition, you can |
| 394 | | * specify an array of regular expressions |
| 395 | | * to be compared to each full source pathname with 'exclude' => array('regexp1', 'regexp2' ...). |
| 396 | | * Note that if a regular expression matches a parent folder then files and subfolders within it |
| 397 | | * will not be synced, regardless of whether they individually match or not. |
| 398 | | * |
| 399 | | * Patterns excluded on the source are also left alone on the destination. |
| 400 | | * |
| 401 | | * This is useful for syncing content to an Amazon S3 wrapper |
| 402 | | * and in other situations where rsync is not available. Does not attempt to |
| 403 | | * set permissions (stream wrappers don't support chmod, for one thing). With our |
| 404 | | * usual s3 configuration you should just use the s3public: protocol for public stuff and |
| 405 | | * the s3private: protocol for private stuff. |
| 406 | | * |
| 407 | | * Symlinks, if encountered, are followed and what they refer to is copied, |
| 408 | | * so don't copy any recursive references |
| 409 | | */ |
| 410 | | static public function sync($from, $to, $options = array()) |
| 411 | | { |
| 412 | | // Let's be verbose for this first big scary migration on staging |
| 413 | | $fromList = aFiles::ls($from); |
| 414 | | if ($fromList === false) |
| 415 | | { |
| 416 | | return false; |
| 417 | | } |
| 418 | | $toList = aFiles::ls($to); |
| 419 | | if ($toList === false) |
| 420 | | { |
| 421 | | if (!mkdir($to)) |
| 422 | | { |
| 423 | | return false; |
| 424 | | } |
| 425 | | $toList = array(); |
| 426 | | } |
| 427 | | $valid = array(); |
| 428 | | foreach ($fromList as $file) |
| 429 | | { |
| 430 | | $fromPath = "$from/$file"; |
| 431 | | if (aFiles::syncExclude($fromPath, $options)) |
| 432 | | { |
| 433 | | continue; |
| 434 | | } |
| 435 | | // Ensure consistency regardless of whether a given system likes trailing slashes on directories |
| 436 | | $valid[preg_replace('/\/$/', '', $file)] = true; |
| 437 | | $toPath = "$to/$file"; |
| 438 | | $fromStat = @stat($fromPath); |
| 439 | | if (!$fromStat) |
| 440 | | { |
| 441 | | error_log("Warning: cannot stat $fromPath, maybe it disappeared in mid-sync?"); |
| 442 | | continue; |
| 443 | | } |
| 444 | | $toStat = @stat($toPath); |
| 445 | | $fromDir = aFiles::statIsDir($fromStat); |
| 446 | | $fromFile = aFiles::statIsFile($fromStat); |
| 447 | | if ($toStat) |
| 448 | | { |
| 449 | | $toDir = aFiles::statIsDir($toStat); |
| 450 | | $toFile = aFiles::statIsFile($toStat); |
| 451 | | if (($toDir !== $fromDir) || ($toFile !== $fromFile)) |
| 452 | | { |
| 453 | | /** |
| 454 | | * Same name but a completely different kind of animal. |
| 455 | | * Remove it on the destination so we'll able to make the |
| 456 | | * other (dir vs. file or vice versa) |
| 457 | | */ |
| 458 | | aFiles::rmRf($toPath); |
| 459 | | } |
| 460 | | elseif ($fromFile) |
| 461 | | { |
| 462 | | if ((!isset($options['force'])) || (!$options['force'])) |
| 463 | | { |
| 464 | | if (($toStat['mtime'] >= $fromStat['mtime']) && ($toStat['size'] === $fromStat['size'])) |
| 465 | | { |
| 466 | | continue; |
| 467 | | } |
| 468 | | } |
| 469 | | } |
| 470 | | } |
| 471 | | if ($fromDir) |
| 472 | | { |
| 473 | | if (!aFiles::sync($fromPath, $toPath, $options)) |
| 474 | | { |
| 475 | | return false; |
| 476 | | } |
| 477 | | } |
| 478 | | else |
| 479 | | { |
| 480 | | if (!aFiles::copy($fromPath, $toPath)) |
| 481 | | { |
| 482 | | error_log("Cannot copy $fromPath to $toPath, maybe it disappeared in mid-sync or receiving drive is full"); |
| 483 | | return false; |
| 484 | | } |
| 485 | | } |
| 486 | | } |
| 487 | | // Remove any files on the destination that did not exist on the source |
| 488 | | foreach ($toList as $file) |
| 489 | | { |
| 490 | | // Remove any inconsistency as to whether a trailing slash is present, |
| 491 | | // otherwise we trash perfectly good folders |
| 492 | | $testFile = preg_replace('/\/$/', '', $file); |
| 493 | | if (!isset($valid[$testFile])) |
| 494 | | { |
| 495 | | $toPath = "$to/$file"; |
| 496 | | if (aFiles::syncExclude($toPath, $options)) |
| 497 | | { |
| 498 | | continue; |
| 499 | | } |
| 500 | | if (!aFiles::rmRf($toPath)) |
| 501 | | { |
| 502 | | error_log("Warning: can't remove $toPath, maybe someone else got rid of it for us"); |
| 503 | | } |
| 504 | | } |
| 505 | | } |
| 506 | | return true; |
| 507 | | } |
| 508 | | |
| 509 | | static public function syncExclude($path, $options) |
| 510 | | { |
| 511 | | if (isset($options['exclude'])) |
| 512 | | { |
| 513 | | $excluded = false; |
| 514 | | // Remove any trailing / before considering patterns. |
| 515 | | // The s3 wrapper appends / to folders to make stat calls faster, |
| 516 | | // but people writing exclude expressions cannot be reasonably |
| 517 | | // expected to consider this |
| 518 | | $excludePath = preg_replace('/\/$/', '', $path); |
| 519 | | foreach ($options['exclude'] as $regexp) |
| 520 | | { |
| 521 | | if (preg_match($regexp, $excludePath)) |
| 522 | | { |
| 523 | | return true; |
| 524 | | } |
| 525 | | } |
| 526 | | } |
| 527 | | return false; |
| 528 | | } |
| 529 | | |
| 530 | | /** |
| 531 | | * Recursively copy one folder to another. Assumes the second path does not exist. |
| 532 | | * This is a lot faster than a sync because it doesn't have to stat() everything. |
| 533 | | * |
| 534 | | * This is useful for syncing content to an Amazon S3 wrapper |
| 535 | | * and in other situations where rsync is not available. Does not attempt to |
| 536 | | * set permissions (stream wrappers don't support chmod, for one thing). |
| 537 | | * |
| 538 | | * Symlinks, if encountered, are followed and what they refer to is copied, |
| 539 | | * so don't copy any recursive references |
| 540 | | * |
| 541 | | * By default, if any part of the copy fails the whole thing fails and is |
| 542 | | * backed out, leaving nothing at $to. If you don't want this, specify |
| 543 | | * 'continue-on-error' => true as an option and the copy will be as complete |
| 544 | | * as possible in the event that one or more items cannot be copied. Verbose |
| 545 | | * errors are logged to the PHP log in this situation. |
| 546 | | * |
| 547 | | * Returns false if any errors occur, true otherwise. |
| 548 | | */ |
| 549 | | static public function copyFolder($from, $to, $options = array()) |
| 550 | | { |
| 551 | | $continueOnError = isset($options['continue-on-error']) && $options['continue-on-error']; |
| 552 | | $result = true; |
| 553 | | |
| 554 | | $fromList = aFiles::ls($from); |
| 555 | | if ($fromList === false) |
| 556 | | { |
| 557 | | error_log("WARNING: aFiles::ls returns false for $from"); |
| 558 | | $result = false; |
| 559 | | return $result; |
| 560 | | } |
| 561 | | |
| 562 | | foreach ($fromList as $file) |
| 563 | | { |
| 564 | | $fromPath = "$from/$file"; |
| 565 | | $toPath = "$to/$file"; |
| 566 | | if (is_dir($fromPath)) |
| 567 | | { |
| 568 | | $result = aFiles::copyFolder($fromPath, $toPath); |
| 569 | | if (!$result) |
| 570 | | { |
| 571 | | $result = false; |
| 572 | | error_log("WARNING: unable to copy $fromPath to $toPath"); |
| 573 | | if (!$continueOnError) |
| 574 | | { |
| 575 | | // If we fail on any file undo the whole thing |
| 576 | | aFiles::rmRf($to); |
| 577 | | return $result; |
| 578 | | } |
| 579 | | } |
| 580 | | } |
| 581 | | else |
| 582 | | { |
| 583 | | if (!aFiles::copy($fromPath, $toPath)) |
| 584 | | { |
| 585 | | $result = false; |
| 586 | | error_log("WARNING: unable to copy $fromPath to $toPath"); |
| 587 | | if (!$continueOnError) |
| 588 | | { |
| 589 | | // If we fail on any file undo the whole thing |
| 590 | | aFiles::rmRf($to); |
| 591 | | return $result; |
| 592 | | } |
| 593 | | } |
| 594 | | } |
| 595 | | } |
| 596 | | return $result; |
| 597 | | } |
| 598 | | |
| 599 | | /** |
| 600 | | * Copy a file, checking the result of fflush() to make sure it really |
| 601 | | * got there. Native php copy() DOES NOT do this. Returns true if the |
| 602 | | * whole thing actually got there. If not, removes $to and returns false |
| 603 | | */ |
| 604 | | static public function copy($from, $to) |
| 605 | | { |
| 606 | | $in = fopen($from, "rb"); |
| 607 | | if (!$in) |
| 608 | | { |
| 609 | | return false; |
| 610 | | } |
| 611 | | $out = fopen($to, "wb"); |
| 612 | | if (!$out) |
| 613 | | { |
| 614 | | fclose($in); |
| 615 | | return false; |
| 616 | | } |
| 617 | | while (true) |
| 618 | | { |
| 619 | | $buf = fread($in, 65536); |
| 620 | | if ($buf === false) |
| 621 | | { |
| 622 | | // Read failed |
| 623 | | fclose($in); |
| 624 | | fclose($out); |
| 625 | | unlink($to); |
| 626 | | return false; |
| 627 | | } |
| 628 | | if (strlen($buf) === 0) |
| 629 | | { |
| 630 | | // EOF |
| 631 | | break; |
| 632 | | } |
| 633 | | if (fwrite($out, $buf) !== strlen($buf)) |
| 634 | | { |
| 635 | | fclose($in); |
| 636 | | fclose($out); |
| 637 | | unlink($to); |
| 638 | | return false; |
| 639 | | } |
| 640 | | } |
| 641 | | fclose($in); |
| 642 | | if (!aFiles::close($out)) |
| 643 | | { |
| 644 | | unlink($to); |
| 645 | | return false; |
| 646 | | } |
| 647 | | return true; |
| 648 | | } |
| 649 | | |
| 650 | | /** |
| 651 | | * Close a file opened with fopen() and friends, after making sure |
| 652 | | * that the write (if any) has actually been successful by checking |
| 653 | | * the result of fflush(). Returns true only if both fflush and fclose |
| 654 | | * succeed. As of this writing fclose always returns true in PHP even |
| 655 | | * if its implicit flush call fails so we need this for reliable close |
| 656 | | */ |
| 657 | | static public function close($file) |
| 658 | | { |
| 659 | | if (!fflush($file)) |
| 660 | | { |
| 661 | | fclose($file); |
| 662 | | return false; |
| 663 | | } |
| 664 | | // It would be nice if this reported false on a failed implicit flush but it doesn't |
| 665 | | if (!fclose($file)) |
| 666 | | { |
| 667 | | return false; |
| 668 | | } |
| 669 | | return true; |
| 670 | | } |
| | 9 | // replace this class at project level to override methods from BaseaFiles |