annotate core/lib/Drupal/Core/File/FileSystem.php @ 5:12f9dff5fda9 tip

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:34:47 +0100
parents c75dbcec494b
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\File;
Chris@0 4
Chris@5 5 use Drupal\Component\Utility\Unicode;
Chris@5 6 use Drupal\Core\File\Exception\DirectoryNotReadyException;
Chris@5 7 use Drupal\Core\File\Exception\FileException;
Chris@5 8 use Drupal\Core\File\Exception\FileExistsException;
Chris@5 9 use Drupal\Core\File\Exception\FileNotExistsException;
Chris@5 10 use Drupal\Core\File\Exception\FileWriteException;
Chris@5 11 use Drupal\Core\File\Exception\NotRegularFileException;
Chris@0 12 use Drupal\Core\Site\Settings;
Chris@0 13 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
Chris@0 14 use Psr\Log\LoggerInterface;
Chris@0 15
Chris@0 16 /**
Chris@0 17 * Provides helpers to operate on files and stream wrappers.
Chris@0 18 */
Chris@0 19 class FileSystem implements FileSystemInterface {
Chris@0 20
Chris@0 21 /**
Chris@0 22 * Default mode for new directories. See self::chmod().
Chris@0 23 */
Chris@0 24 const CHMOD_DIRECTORY = 0775;
Chris@0 25
Chris@0 26 /**
Chris@0 27 * Default mode for new files. See self::chmod().
Chris@0 28 */
Chris@0 29 const CHMOD_FILE = 0664;
Chris@0 30
Chris@0 31 /**
Chris@0 32 * The site settings.
Chris@0 33 *
Chris@0 34 * @var \Drupal\Core\Site\Settings
Chris@0 35 */
Chris@0 36 protected $settings;
Chris@0 37
Chris@0 38 /**
Chris@0 39 * The file logger channel.
Chris@0 40 *
Chris@0 41 * @var \Psr\Log\LoggerInterface
Chris@0 42 */
Chris@0 43 protected $logger;
Chris@0 44
Chris@0 45 /**
Chris@0 46 * The stream wrapper manager.
Chris@0 47 *
Chris@0 48 * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
Chris@0 49 */
Chris@0 50 protected $streamWrapperManager;
Chris@0 51
Chris@0 52 /**
Chris@0 53 * Constructs a new FileSystem.
Chris@0 54 *
Chris@0 55 * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
Chris@0 56 * The stream wrapper manager.
Chris@0 57 * @param \Drupal\Core\Site\Settings $settings
Chris@0 58 * The site settings.
Chris@0 59 * @param \Psr\Log\LoggerInterface $logger
Chris@0 60 * The file logger channel.
Chris@0 61 */
Chris@0 62 public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager, Settings $settings, LoggerInterface $logger) {
Chris@0 63 $this->streamWrapperManager = $stream_wrapper_manager;
Chris@0 64 $this->settings = $settings;
Chris@0 65 $this->logger = $logger;
Chris@0 66 }
Chris@0 67
Chris@0 68 /**
Chris@0 69 * {@inheritdoc}
Chris@0 70 */
Chris@0 71 public function moveUploadedFile($filename, $uri) {
Chris@0 72 $result = @move_uploaded_file($filename, $uri);
Chris@0 73 // PHP's move_uploaded_file() does not properly support streams if
Chris@0 74 // open_basedir is enabled so if the move failed, try finding a real path
Chris@0 75 // and retry the move operation.
Chris@0 76 if (!$result) {
Chris@0 77 if ($realpath = $this->realpath($uri)) {
Chris@0 78 $result = move_uploaded_file($filename, $realpath);
Chris@0 79 }
Chris@0 80 else {
Chris@0 81 $result = move_uploaded_file($filename, $uri);
Chris@0 82 }
Chris@0 83 }
Chris@0 84
Chris@0 85 return $result;
Chris@0 86 }
Chris@0 87
Chris@0 88 /**
Chris@0 89 * {@inheritdoc}
Chris@0 90 */
Chris@0 91 public function chmod($uri, $mode = NULL) {
Chris@0 92 if (!isset($mode)) {
Chris@0 93 if (is_dir($uri)) {
Chris@0 94 $mode = $this->settings->get('file_chmod_directory', static::CHMOD_DIRECTORY);
Chris@0 95 }
Chris@0 96 else {
Chris@0 97 $mode = $this->settings->get('file_chmod_file', static::CHMOD_FILE);
Chris@0 98 }
Chris@0 99 }
Chris@0 100
Chris@0 101 if (@chmod($uri, $mode)) {
Chris@0 102 return TRUE;
Chris@0 103 }
Chris@0 104
Chris@0 105 $this->logger->error('The file permissions could not be set on %uri.', ['%uri' => $uri]);
Chris@0 106 return FALSE;
Chris@0 107 }
Chris@0 108
Chris@0 109 /**
Chris@0 110 * {@inheritdoc}
Chris@0 111 */
Chris@0 112 public function unlink($uri, $context = NULL) {
Chris@0 113 $scheme = $this->uriScheme($uri);
Chris@0 114 if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) {
Chris@0 115 chmod($uri, 0600);
Chris@0 116 }
Chris@0 117 if ($context) {
Chris@0 118 return unlink($uri, $context);
Chris@0 119 }
Chris@0 120 else {
Chris@0 121 return unlink($uri);
Chris@0 122 }
Chris@0 123 }
Chris@0 124
Chris@0 125 /**
Chris@0 126 * {@inheritdoc}
Chris@0 127 */
Chris@0 128 public function realpath($uri) {
Chris@0 129 // If this URI is a stream, pass it off to the appropriate stream wrapper.
Chris@0 130 // Otherwise, attempt PHP's realpath. This allows use of this method even
Chris@0 131 // for unmanaged files outside of the stream wrapper interface.
Chris@0 132 if ($wrapper = $this->streamWrapperManager->getViaUri($uri)) {
Chris@0 133 return $wrapper->realpath();
Chris@0 134 }
Chris@0 135
Chris@0 136 return realpath($uri);
Chris@0 137 }
Chris@0 138
Chris@0 139 /**
Chris@0 140 * {@inheritdoc}
Chris@0 141 */
Chris@0 142 public function dirname($uri) {
Chris@0 143 $scheme = $this->uriScheme($uri);
Chris@0 144
Chris@0 145 if ($this->validScheme($scheme)) {
Chris@0 146 return $this->streamWrapperManager->getViaScheme($scheme)->dirname($uri);
Chris@0 147 }
Chris@0 148 else {
Chris@0 149 return dirname($uri);
Chris@0 150 }
Chris@0 151 }
Chris@0 152
Chris@0 153 /**
Chris@0 154 * {@inheritdoc}
Chris@0 155 */
Chris@0 156 public function basename($uri, $suffix = NULL) {
Chris@0 157 $separators = '/';
Chris@0 158 if (DIRECTORY_SEPARATOR != '/') {
Chris@0 159 // For Windows OS add special separator.
Chris@0 160 $separators .= DIRECTORY_SEPARATOR;
Chris@0 161 }
Chris@0 162 // Remove right-most slashes when $uri points to directory.
Chris@0 163 $uri = rtrim($uri, $separators);
Chris@0 164 // Returns the trailing part of the $uri starting after one of the directory
Chris@0 165 // separators.
Chris@0 166 $filename = preg_match('@[^' . preg_quote($separators, '@') . ']+$@', $uri, $matches) ? $matches[0] : '';
Chris@0 167 // Cuts off a suffix from the filename.
Chris@0 168 if ($suffix) {
Chris@0 169 $filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename);
Chris@0 170 }
Chris@0 171 return $filename;
Chris@0 172 }
Chris@0 173
Chris@0 174 /**
Chris@0 175 * {@inheritdoc}
Chris@0 176 */
Chris@0 177 public function mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
Chris@0 178 if (!isset($mode)) {
Chris@0 179 $mode = $this->settings->get('file_chmod_directory', static::CHMOD_DIRECTORY);
Chris@0 180 }
Chris@0 181
Chris@0 182 // If the URI has a scheme, don't override the umask - schemes can handle
Chris@0 183 // this issue in their own implementation.
Chris@0 184 if ($this->uriScheme($uri)) {
Chris@0 185 return $this->mkdirCall($uri, $mode, $recursive, $context);
Chris@0 186 }
Chris@0 187
Chris@0 188 // If recursive, create each missing component of the parent directory
Chris@0 189 // individually and set the mode explicitly to override the umask.
Chris@0 190 if ($recursive) {
Chris@0 191 // Ensure the path is using DIRECTORY_SEPARATOR, and trim off any trailing
Chris@0 192 // slashes because they can throw off the loop when creating the parent
Chris@0 193 // directories.
Chris@0 194 $uri = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $uri), DIRECTORY_SEPARATOR);
Chris@0 195 // Determine the components of the path.
Chris@0 196 $components = explode(DIRECTORY_SEPARATOR, $uri);
Chris@0 197 // If the filepath is absolute the first component will be empty as there
Chris@0 198 // will be nothing before the first slash.
Chris@0 199 if ($components[0] == '') {
Chris@0 200 $recursive_path = DIRECTORY_SEPARATOR;
Chris@0 201 // Get rid of the empty first component.
Chris@0 202 array_shift($components);
Chris@0 203 }
Chris@0 204 else {
Chris@0 205 $recursive_path = '';
Chris@0 206 }
Chris@0 207 // Don't handle the top-level directory in this loop.
Chris@0 208 array_pop($components);
Chris@0 209 // Create each component if necessary.
Chris@0 210 foreach ($components as $component) {
Chris@0 211 $recursive_path .= $component;
Chris@0 212
Chris@0 213 if (!file_exists($recursive_path)) {
Chris@0 214 if (!$this->mkdirCall($recursive_path, $mode, FALSE, $context)) {
Chris@0 215 return FALSE;
Chris@0 216 }
Chris@0 217 // Not necessary to use self::chmod() as there is no scheme.
Chris@0 218 if (!chmod($recursive_path, $mode)) {
Chris@0 219 return FALSE;
Chris@0 220 }
Chris@0 221 }
Chris@0 222
Chris@0 223 $recursive_path .= DIRECTORY_SEPARATOR;
Chris@0 224 }
Chris@0 225 }
Chris@0 226
Chris@0 227 // Do not check if the top-level directory already exists, as this condition
Chris@0 228 // must cause this function to fail.
Chris@0 229 if (!$this->mkdirCall($uri, $mode, FALSE, $context)) {
Chris@0 230 return FALSE;
Chris@0 231 }
Chris@0 232 // Not necessary to use self::chmod() as there is no scheme.
Chris@0 233 return chmod($uri, $mode);
Chris@0 234 }
Chris@0 235
Chris@0 236 /**
Chris@0 237 * Helper function. Ensures we don't pass a NULL as a context resource to
Chris@0 238 * mkdir().
Chris@0 239 *
Chris@0 240 * @see self::mkdir()
Chris@0 241 */
Chris@0 242 protected function mkdirCall($uri, $mode, $recursive, $context) {
Chris@0 243 if (is_null($context)) {
Chris@0 244 return mkdir($uri, $mode, $recursive);
Chris@0 245 }
Chris@0 246 else {
Chris@0 247 return mkdir($uri, $mode, $recursive, $context);
Chris@0 248 }
Chris@0 249 }
Chris@0 250
Chris@0 251 /**
Chris@0 252 * {@inheritdoc}
Chris@0 253 */
Chris@0 254 public function rmdir($uri, $context = NULL) {
Chris@0 255 $scheme = $this->uriScheme($uri);
Chris@0 256 if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) {
Chris@0 257 chmod($uri, 0700);
Chris@0 258 }
Chris@0 259 if ($context) {
Chris@0 260 return rmdir($uri, $context);
Chris@0 261 }
Chris@0 262 else {
Chris@0 263 return rmdir($uri);
Chris@0 264 }
Chris@0 265 }
Chris@0 266
Chris@0 267 /**
Chris@0 268 * {@inheritdoc}
Chris@0 269 */
Chris@0 270 public function tempnam($directory, $prefix) {
Chris@0 271 $scheme = $this->uriScheme($directory);
Chris@0 272
Chris@0 273 if ($this->validScheme($scheme)) {
Chris@0 274 $wrapper = $this->streamWrapperManager->getViaScheme($scheme);
Chris@0 275
Chris@0 276 if ($filename = tempnam($wrapper->getDirectoryPath(), $prefix)) {
Chris@0 277 return $scheme . '://' . static::basename($filename);
Chris@0 278 }
Chris@0 279 else {
Chris@0 280 return FALSE;
Chris@0 281 }
Chris@0 282 }
Chris@0 283 else {
Chris@0 284 // Handle as a normal tempnam() call.
Chris@0 285 return tempnam($directory, $prefix);
Chris@0 286 }
Chris@0 287 }
Chris@0 288
Chris@0 289 /**
Chris@0 290 * {@inheritdoc}
Chris@0 291 */
Chris@0 292 public function uriScheme($uri) {
Chris@0 293 if (preg_match('/^([\w\-]+):\/\/|^(data):/', $uri, $matches)) {
Chris@0 294 // The scheme will always be the last element in the matches array.
Chris@0 295 return array_pop($matches);
Chris@0 296 }
Chris@0 297
Chris@0 298 return FALSE;
Chris@0 299 }
Chris@0 300
Chris@0 301 /**
Chris@0 302 * {@inheritdoc}
Chris@0 303 */
Chris@0 304 public function validScheme($scheme) {
Chris@0 305 if (!$scheme) {
Chris@0 306 return FALSE;
Chris@0 307 }
Chris@0 308 return class_exists($this->streamWrapperManager->getClass($scheme));
Chris@0 309 }
Chris@0 310
Chris@5 311 /**
Chris@5 312 * {@inheritdoc}
Chris@5 313 */
Chris@5 314 public function copy($source, $destination, $replace = self::EXISTS_RENAME) {
Chris@5 315 $this->prepareDestination($source, $destination, $replace);
Chris@5 316
Chris@5 317 // Perform the copy operation.
Chris@5 318 if (!@copy($source, $destination)) {
Chris@5 319 // If the copy failed and realpaths exist, retry the operation using them
Chris@5 320 // instead.
Chris@5 321 $real_source = $this->realpath($source) ?: $source;
Chris@5 322 $real_destination = $this->realpath($destination) ?: $destination;
Chris@5 323 if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
Chris@5 324 $this->logger->error("The specified file '%source' could not be copied to '%destination'.", [
Chris@5 325 '%source' => $source,
Chris@5 326 '%destination' => $destination,
Chris@5 327 ]);
Chris@5 328 throw new FileWriteException("The specified file '$source' could not be copied to '$destination'.");
Chris@5 329 }
Chris@5 330 }
Chris@5 331
Chris@5 332 // Set the permissions on the new file.
Chris@5 333 $this->chmod($destination);
Chris@5 334
Chris@5 335 return $destination;
Chris@5 336 }
Chris@5 337
Chris@5 338 /**
Chris@5 339 * {@inheritdoc}
Chris@5 340 */
Chris@5 341 public function delete($path) {
Chris@5 342 if (is_file($path)) {
Chris@5 343 if (!$this->unlink($path)) {
Chris@5 344 $this->logger->error("Failed to unlink file '%path'.", ['%path' => $path]);
Chris@5 345 throw new FileException("Failed to unlink file '$path'.");
Chris@5 346 }
Chris@5 347 return TRUE;
Chris@5 348 }
Chris@5 349
Chris@5 350 if (is_dir($path)) {
Chris@5 351 $this->logger->error("Cannot delete '%path' because it is a directory. Use deleteRecursive() instead.", ['%path' => $path]);
Chris@5 352 throw new NotRegularFileException("Cannot delete '$path' because it is a directory. Use deleteRecursive() instead.");
Chris@5 353 }
Chris@5 354
Chris@5 355 // Return TRUE for non-existent file, but log that nothing was actually
Chris@5 356 // deleted, as the current state is the intended result.
Chris@5 357 if (!file_exists($path)) {
Chris@5 358 $this->logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
Chris@5 359 return TRUE;
Chris@5 360 }
Chris@5 361
Chris@5 362 // We cannot handle anything other than files and directories.
Chris@5 363 // Throw an exception for everything else (sockets, symbolic links, etc).
Chris@5 364 $this->logger->error("The file '%path' is not of a recognized type so it was not deleted.", ['%path' => $path]);
Chris@5 365 throw new NotRegularFileException("The file '$path' is not of a recognized type so it was not deleted.");
Chris@5 366 }
Chris@5 367
Chris@5 368 /**
Chris@5 369 * {@inheritdoc}
Chris@5 370 */
Chris@5 371 public function deleteRecursive($path, callable $callback = NULL) {
Chris@5 372 if ($callback) {
Chris@5 373 call_user_func($callback, $path);
Chris@5 374 }
Chris@5 375
Chris@5 376 if (is_dir($path)) {
Chris@5 377 $dir = dir($path);
Chris@5 378 while (($entry = $dir->read()) !== FALSE) {
Chris@5 379 if ($entry == '.' || $entry == '..') {
Chris@5 380 continue;
Chris@5 381 }
Chris@5 382 $entry_path = $path . '/' . $entry;
Chris@5 383 $this->deleteRecursive($entry_path, $callback);
Chris@5 384 }
Chris@5 385 $dir->close();
Chris@5 386
Chris@5 387 return $this->rmdir($path);
Chris@5 388 }
Chris@5 389
Chris@5 390 return $this->delete($path);
Chris@5 391 }
Chris@5 392
Chris@5 393 /**
Chris@5 394 * {@inheritdoc}
Chris@5 395 */
Chris@5 396 public function move($source, $destination, $replace = self::EXISTS_RENAME) {
Chris@5 397 $this->prepareDestination($source, $destination, $replace);
Chris@5 398
Chris@5 399 // Ensure compatibility with Windows.
Chris@5 400 // @see \Drupal\Core\File\FileSystemInterface::unlink().
Chris@5 401 $scheme = $this->uriScheme($source);
Chris@5 402 if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) {
Chris@5 403 chmod($source, 0600);
Chris@5 404 }
Chris@5 405 // Attempt to resolve the URIs. This is necessary in certain
Chris@5 406 // configurations (see above) and can also permit fast moves across local
Chris@5 407 // schemes.
Chris@5 408 $real_source = $this->realpath($source) ?: $source;
Chris@5 409 $real_destination = $this->realpath($destination) ?: $destination;
Chris@5 410 // Perform the move operation.
Chris@5 411 if (!@rename($real_source, $real_destination)) {
Chris@5 412 // Fall back to slow copy and unlink procedure. This is necessary for
Chris@5 413 // renames across schemes that are not local, or where rename() has not
Chris@5 414 // been implemented. It's not necessary to use drupal_unlink() as the
Chris@5 415 // Windows issue has already been resolved above.
Chris@5 416 if (!@copy($real_source, $real_destination)) {
Chris@5 417 $this->logger->error("The specified file '%source' could not be moved to '%destination'.", [
Chris@5 418 '%source' => $source,
Chris@5 419 '%destination' => $destination,
Chris@5 420 ]);
Chris@5 421 throw new FileWriteException("The specified file '$source' could not be moved to '$destination'.");
Chris@5 422 }
Chris@5 423 if (!@unlink($real_source)) {
Chris@5 424 $this->logger->error("The source file '%source' could not be unlinked after copying to '%destination'.", [
Chris@5 425 '%source' => $source,
Chris@5 426 '%destination' => $destination,
Chris@5 427 ]);
Chris@5 428 throw new FileException("The source file '$source' could not be unlinked after copying to '$destination'.");
Chris@5 429 }
Chris@5 430 }
Chris@5 431
Chris@5 432 // Set the permissions on the new file.
Chris@5 433 $this->chmod($destination);
Chris@5 434
Chris@5 435 return $destination;
Chris@5 436 }
Chris@5 437
Chris@5 438 /**
Chris@5 439 * Prepares the destination for a file copy or move operation.
Chris@5 440 *
Chris@5 441 * - Checks if $source and $destination are valid and readable/writable.
Chris@5 442 * - Checks that $source is not equal to $destination; if they are an error
Chris@5 443 * is reported.
Chris@5 444 * - If file already exists in $destination either the call will error out,
Chris@5 445 * replace the file or rename the file based on the $replace parameter.
Chris@5 446 *
Chris@5 447 * @param string $source
Chris@5 448 * A string specifying the filepath or URI of the source file.
Chris@5 449 * @param string|null $destination
Chris@5 450 * A URI containing the destination that $source should be moved/copied to.
Chris@5 451 * The URI may be a bare filepath (without a scheme) and in that case the
Chris@5 452 * default scheme (file://) will be used.
Chris@5 453 * @param int $replace
Chris@5 454 * Replace behavior when the destination file already exists:
Chris@5 455 * - FILE_EXISTS_REPLACE - Replace the existing file.
Chris@5 456 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename
Chris@5 457 * is unique.
Chris@5 458 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
Chris@5 459 *
Chris@5 460 * @see \Drupal\Core\File\FileSystemInterface::copy()
Chris@5 461 * @see \Drupal\Core\File\FileSystemInterface::move()
Chris@5 462 */
Chris@5 463 protected function prepareDestination($source, &$destination, $replace) {
Chris@5 464 $original_source = $source;
Chris@5 465
Chris@5 466 // Assert that the source file actually exists.
Chris@5 467 if (!file_exists($source)) {
Chris@5 468 if (($realpath = $this->realpath($original_source)) !== FALSE) {
Chris@5 469 $this->logger->error("File '%original_source' ('%realpath') could not be copied because it does not exist.", [
Chris@5 470 '%original_source' => $original_source,
Chris@5 471 '%realpath' => $realpath,
Chris@5 472 ]);
Chris@5 473 throw new FileNotExistsException("File '$original_source' ('$realpath') could not be copied because it does not exist.");
Chris@5 474 }
Chris@5 475 else {
Chris@5 476 $this->logger->error("File '%original_source' could not be copied because it does not exist.", [
Chris@5 477 '%original_source' => $original_source,
Chris@5 478 ]);
Chris@5 479 throw new FileNotExistsException("File '$original_source' could not be copied because it does not exist.");
Chris@5 480 }
Chris@5 481 }
Chris@5 482
Chris@5 483 // Prepare the destination directory.
Chris@5 484 if ($this->prepareDirectory($destination)) {
Chris@5 485 // The destination is already a directory, so append the source basename.
Chris@5 486 $destination = file_stream_wrapper_uri_normalize($destination . '/' . $this->basename($source));
Chris@5 487 }
Chris@5 488 else {
Chris@5 489 // Perhaps $destination is a dir/file?
Chris@5 490 $dirname = $this->dirname($destination);
Chris@5 491 if (!$this->prepareDirectory($dirname)) {
Chris@5 492 $this->logger->error("The specified file '%original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.", [
Chris@5 493 '%original_source' => $original_source,
Chris@5 494 ]);
Chris@5 495 throw new DirectoryNotReadyException("The specified file '$original_source' could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions.");
Chris@5 496 }
Chris@5 497 }
Chris@5 498
Chris@5 499 // Determine whether we can perform this operation based on overwrite rules.
Chris@5 500 $destination = $this->getDestinationFilename($destination, $replace);
Chris@5 501 if ($destination === FALSE) {
Chris@5 502 $this->logger->error("File '%original_source' could not be copied because a file by that name already exists in the destination directory ('%destination').", [
Chris@5 503 '%original_source' => $original_source,
Chris@5 504 '%destination' => $destination,
Chris@5 505 ]);
Chris@5 506 throw new FileExistsException("File '$original_source' could not be copied because a file by that name already exists in the destination directory ('$destination').");
Chris@5 507 }
Chris@5 508
Chris@5 509 // Assert that the source and destination filenames are not the same.
Chris@5 510 $real_source = $this->realpath($source);
Chris@5 511 $real_destination = $this->realpath($destination);
Chris@5 512 if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
Chris@5 513 $this->logger->error("File '%source' could not be copied because it would overwrite itself.", [
Chris@5 514 '%source' => $source,
Chris@5 515 ]);
Chris@5 516 throw new FileException("File '$source' could not be copied because it would overwrite itself.");
Chris@5 517 }
Chris@5 518 // Make sure the .htaccess files are present.
Chris@5 519 // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304.
Chris@5 520 file_ensure_htaccess();
Chris@5 521 }
Chris@5 522
Chris@5 523 /**
Chris@5 524 * {@inheritdoc}
Chris@5 525 */
Chris@5 526 public function saveData($data, $destination, $replace = self::EXISTS_RENAME) {
Chris@5 527 // Write the data to a temporary file.
Chris@5 528 $temp_name = $this->tempnam('temporary://', 'file');
Chris@5 529 if (file_put_contents($temp_name, $data) === FALSE) {
Chris@5 530 $this->logger->error("Temporary file '%temp_name' could not be created.", ['%temp_name' => $temp_name]);
Chris@5 531 throw new FileWriteException("Temporary file '$temp_name' could not be created.");
Chris@5 532 }
Chris@5 533
Chris@5 534 // Move the file to its final destination.
Chris@5 535 return $this->move($temp_name, $destination, $replace);
Chris@5 536 }
Chris@5 537
Chris@5 538 /**
Chris@5 539 * {@inheritdoc}
Chris@5 540 */
Chris@5 541 public function prepareDirectory(&$directory, $options = self::MODIFY_PERMISSIONS) {
Chris@5 542 if (!$this->validScheme($this->uriScheme($directory))) {
Chris@5 543 // Only trim if we're not dealing with a stream.
Chris@5 544 $directory = rtrim($directory, '/\\');
Chris@5 545 }
Chris@5 546
Chris@5 547 // Check if directory exists.
Chris@5 548 if (!is_dir($directory)) {
Chris@5 549 // Let mkdir() recursively create directories and use the default
Chris@5 550 // directory permissions.
Chris@5 551 if ($options & static::CREATE_DIRECTORY) {
Chris@5 552 return @$this->mkdir($directory, NULL, TRUE);
Chris@5 553 }
Chris@5 554 return FALSE;
Chris@5 555 }
Chris@5 556 // The directory exists, so check to see if it is writable.
Chris@5 557 $writable = is_writable($directory);
Chris@5 558 if (!$writable && ($options & static::MODIFY_PERMISSIONS)) {
Chris@5 559 return $this->chmod($directory);
Chris@5 560 }
Chris@5 561
Chris@5 562 return $writable;
Chris@5 563 }
Chris@5 564
Chris@5 565 /**
Chris@5 566 * {@inheritdoc}
Chris@5 567 */
Chris@5 568 public function getDestinationFilename($destination, $replace) {
Chris@5 569 $basename = $this->basename($destination);
Chris@5 570 if (!Unicode::validateUtf8($basename)) {
Chris@5 571 throw new FileException(sprintf("Invalid filename '%s'", $basename));
Chris@5 572 }
Chris@5 573 if (file_exists($destination)) {
Chris@5 574 switch ($replace) {
Chris@5 575 case FileSystemInterface::EXISTS_REPLACE:
Chris@5 576 // Do nothing here, we want to overwrite the existing file.
Chris@5 577 break;
Chris@5 578
Chris@5 579 case FileSystemInterface::EXISTS_RENAME:
Chris@5 580 $directory = $this->dirname($destination);
Chris@5 581 $destination = $this->createFilename($basename, $directory);
Chris@5 582 break;
Chris@5 583
Chris@5 584 case FileSystemInterface::EXISTS_ERROR:
Chris@5 585 // Error reporting handled by calling function.
Chris@5 586 return FALSE;
Chris@5 587 }
Chris@5 588 }
Chris@5 589 return $destination;
Chris@5 590 }
Chris@5 591
Chris@5 592 /**
Chris@5 593 * {@inheritdoc}
Chris@5 594 */
Chris@5 595 public function createFilename($basename, $directory) {
Chris@5 596 $original = $basename;
Chris@5 597 // Strip control characters (ASCII value < 32). Though these are allowed in
Chris@5 598 // some filesystems, not many applications handle them well.
Chris@5 599 $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
Chris@5 600 if (preg_last_error() !== PREG_NO_ERROR) {
Chris@5 601 throw new FileException(sprintf("Invalid filename '%s'", $original));
Chris@5 602 }
Chris@5 603 if (substr(PHP_OS, 0, 3) == 'WIN') {
Chris@5 604 // These characters are not allowed in Windows filenames.
Chris@5 605 $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
Chris@5 606 }
Chris@5 607
Chris@5 608 // A URI or path may already have a trailing slash or look like "public://".
Chris@5 609 if (substr($directory, -1) == '/') {
Chris@5 610 $separator = '';
Chris@5 611 }
Chris@5 612 else {
Chris@5 613 $separator = '/';
Chris@5 614 }
Chris@5 615
Chris@5 616 $destination = $directory . $separator . $basename;
Chris@5 617
Chris@5 618 if (file_exists($destination)) {
Chris@5 619 // Destination file already exists, generate an alternative.
Chris@5 620 $pos = strrpos($basename, '.');
Chris@5 621 if ($pos !== FALSE) {
Chris@5 622 $name = substr($basename, 0, $pos);
Chris@5 623 $ext = substr($basename, $pos);
Chris@5 624 }
Chris@5 625 else {
Chris@5 626 $name = $basename;
Chris@5 627 $ext = '';
Chris@5 628 }
Chris@5 629
Chris@5 630 $counter = 0;
Chris@5 631 do {
Chris@5 632 $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
Chris@5 633 } while (file_exists($destination));
Chris@5 634 }
Chris@5 635
Chris@5 636 return $destination;
Chris@5 637 }
Chris@5 638
Chris@0 639 }