annotate core/lib/Drupal/Core/File/FileSystem.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\File;
Chris@0 4
Chris@18 5 use Drupal\Component\Utility\Unicode;
Chris@18 6 use Drupal\Core\File\Exception\DirectoryNotReadyException;
Chris@18 7 use Drupal\Core\File\Exception\FileException;
Chris@18 8 use Drupal\Core\File\Exception\FileExistsException;
Chris@18 9 use Drupal\Core\File\Exception\FileNotExistsException;
Chris@18 10 use Drupal\Core\File\Exception\FileWriteException;
Chris@18 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@18 311 /**
Chris@18 312 * {@inheritdoc}
Chris@18 313 */
Chris@18 314 public function copy($source, $destination, $replace = self::EXISTS_RENAME) {
Chris@18 315 $this->prepareDestination($source, $destination, $replace);
Chris@18 316
Chris@18 317 // Perform the copy operation.
Chris@18 318 if (!@copy($source, $destination)) {
Chris@18 319 // If the copy failed and realpaths exist, retry the operation using them
Chris@18 320 // instead.
Chris@18 321 $real_source = $this->realpath($source) ?: $source;
Chris@18 322 $real_destination = $this->realpath($destination) ?: $destination;
Chris@18 323 if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
Chris@18 324 $this->logger->error("The specified file '%source' could not be copied to '%destination'.", [
Chris@18 325 '%source' => $source,
Chris@18 326 '%destination' => $destination,
Chris@18 327 ]);
Chris@18 328 throw new FileWriteException("The specified file '$source' could not be copied to '$destination'.");
Chris@18 329 }
Chris@18 330 }
Chris@18 331
Chris@18 332 // Set the permissions on the new file.
Chris@18 333 $this->chmod($destination);
Chris@18 334
Chris@18 335 return $destination;
Chris@18 336 }
Chris@18 337
Chris@18 338 /**
Chris@18 339 * {@inheritdoc}
Chris@18 340 */
Chris@18 341 public function delete($path) {
Chris@18 342 if (is_file($path)) {
Chris@18 343 if (!$this->unlink($path)) {
Chris@18 344 $this->logger->error("Failed to unlink file '%path'.", ['%path' => $path]);
Chris@18 345 throw new FileException("Failed to unlink file '$path'.");
Chris@18 346 }
Chris@18 347 return TRUE;
Chris@18 348 }
Chris@18 349
Chris@18 350 if (is_dir($path)) {
Chris@18 351 $this->logger->error("Cannot delete '%path' because it is a directory. Use deleteRecursive() instead.", ['%path' => $path]);
Chris@18 352 throw new NotRegularFileException("Cannot delete '$path' because it is a directory. Use deleteRecursive() instead.");
Chris@18 353 }
Chris@18 354
Chris@18 355 // Return TRUE for non-existent file, but log that nothing was actually
Chris@18 356 // deleted, as the current state is the intended result.
Chris@18 357 if (!file_exists($path)) {
Chris@18 358 $this->logger->notice('The file %path was not deleted because it does not exist.', ['%path' => $path]);
Chris@18 359 return TRUE;
Chris@18 360 }
Chris@18 361
Chris@18 362 // We cannot handle anything other than files and directories.
Chris@18 363 // Throw an exception for everything else (sockets, symbolic links, etc).
Chris@18 364 $this->logger->error("The file '%path' is not of a recognized type so it was not deleted.", ['%path' => $path]);
Chris@18 365 throw new NotRegularFileException("The file '$path' is not of a recognized type so it was not deleted.");
Chris@18 366 }
Chris@18 367
Chris@18 368 /**
Chris@18 369 * {@inheritdoc}
Chris@18 370 */
Chris@18 371 public function deleteRecursive($path, callable $callback = NULL) {
Chris@18 372 if ($callback) {
Chris@18 373 call_user_func($callback, $path);
Chris@18 374 }
Chris@18 375
Chris@18 376 if (is_dir($path)) {
Chris@18 377 $dir = dir($path);
Chris@18 378 while (($entry = $dir->read()) !== FALSE) {
Chris@18 379 if ($entry == '.' || $entry == '..') {
Chris@18 380 continue;
Chris@18 381 }
Chris@18 382 $entry_path = $path . '/' . $entry;
Chris@18 383 $this->deleteRecursive($entry_path, $callback);
Chris@18 384 }
Chris@18 385 $dir->close();
Chris@18 386
Chris@18 387 return $this->rmdir($path);
Chris@18 388 }
Chris@18 389
Chris@18 390 return $this->delete($path);
Chris@18 391 }
Chris@18 392
Chris@18 393 /**
Chris@18 394 * {@inheritdoc}
Chris@18 395 */
Chris@18 396 public function move($source, $destination, $replace = self::EXISTS_RENAME) {
Chris@18 397 $this->prepareDestination($source, $destination, $replace);
Chris@18 398
Chris@18 399 // Ensure compatibility with Windows.
Chris@18 400 // @see \Drupal\Core\File\FileSystemInterface::unlink().
Chris@18 401 $scheme = $this->uriScheme($source);
Chris@18 402 if (!$this->validScheme($scheme) && (substr(PHP_OS, 0, 3) == 'WIN')) {
Chris@18 403 chmod($source, 0600);
Chris@18 404 }
Chris@18 405 // Attempt to resolve the URIs. This is necessary in certain
Chris@18 406 // configurations (see above) and can also permit fast moves across local
Chris@18 407 // schemes.
Chris@18 408 $real_source = $this->realpath($source) ?: $source;
Chris@18 409 $real_destination = $this->realpath($destination) ?: $destination;
Chris@18 410 // Perform the move operation.
Chris@18 411 if (!@rename($real_source, $real_destination)) {
Chris@18 412 // Fall back to slow copy and unlink procedure. This is necessary for
Chris@18 413 // renames across schemes that are not local, or where rename() has not
Chris@18 414 // been implemented. It's not necessary to use drupal_unlink() as the
Chris@18 415 // Windows issue has already been resolved above.
Chris@18 416 if (!@copy($real_source, $real_destination)) {
Chris@18 417 $this->logger->error("The specified file '%source' could not be moved to '%destination'.", [
Chris@18 418 '%source' => $source,
Chris@18 419 '%destination' => $destination,
Chris@18 420 ]);
Chris@18 421 throw new FileWriteException("The specified file '$source' could not be moved to '$destination'.");
Chris@18 422 }
Chris@18 423 if (!@unlink($real_source)) {
Chris@18 424 $this->logger->error("The source file '%source' could not be unlinked after copying to '%destination'.", [
Chris@18 425 '%source' => $source,
Chris@18 426 '%destination' => $destination,
Chris@18 427 ]);
Chris@18 428 throw new FileException("The source file '$source' could not be unlinked after copying to '$destination'.");
Chris@18 429 }
Chris@18 430 }
Chris@18 431
Chris@18 432 // Set the permissions on the new file.
Chris@18 433 $this->chmod($destination);
Chris@18 434
Chris@18 435 return $destination;
Chris@18 436 }
Chris@18 437
Chris@18 438 /**
Chris@18 439 * Prepares the destination for a file copy or move operation.
Chris@18 440 *
Chris@18 441 * - Checks if $source and $destination are valid and readable/writable.
Chris@18 442 * - Checks that $source is not equal to $destination; if they are an error
Chris@18 443 * is reported.
Chris@18 444 * - If file already exists in $destination either the call will error out,
Chris@18 445 * replace the file or rename the file based on the $replace parameter.
Chris@18 446 *
Chris@18 447 * @param string $source
Chris@18 448 * A string specifying the filepath or URI of the source file.
Chris@18 449 * @param string|null $destination
Chris@18 450 * A URI containing the destination that $source should be moved/copied to.
Chris@18 451 * The URI may be a bare filepath (without a scheme) and in that case the
Chris@18 452 * default scheme (file://) will be used.
Chris@18 453 * @param int $replace
Chris@18 454 * Replace behavior when the destination file already exists:
Chris@18 455 * - FILE_EXISTS_REPLACE - Replace the existing file.
Chris@18 456 * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename
Chris@18 457 * is unique.
Chris@18 458 * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
Chris@18 459 *
Chris@18 460 * @see \Drupal\Core\File\FileSystemInterface::copy()
Chris@18 461 * @see \Drupal\Core\File\FileSystemInterface::move()
Chris@18 462 */
Chris@18 463 protected function prepareDestination($source, &$destination, $replace) {
Chris@18 464 $original_source = $source;
Chris@18 465
Chris@18 466 // Assert that the source file actually exists.
Chris@18 467 if (!file_exists($source)) {
Chris@18 468 if (($realpath = $this->realpath($original_source)) !== FALSE) {
Chris@18 469 $this->logger->error("File '%original_source' ('%realpath') could not be copied because it does not exist.", [
Chris@18 470 '%original_source' => $original_source,
Chris@18 471 '%realpath' => $realpath,
Chris@18 472 ]);
Chris@18 473 throw new FileNotExistsException("File '$original_source' ('$realpath') could not be copied because it does not exist.");
Chris@18 474 }
Chris@18 475 else {
Chris@18 476 $this->logger->error("File '%original_source' could not be copied because it does not exist.", [
Chris@18 477 '%original_source' => $original_source,
Chris@18 478 ]);
Chris@18 479 throw new FileNotExistsException("File '$original_source' could not be copied because it does not exist.");
Chris@18 480 }
Chris@18 481 }
Chris@18 482
Chris@18 483 // Prepare the destination directory.
Chris@18 484 if ($this->prepareDirectory($destination)) {
Chris@18 485 // The destination is already a directory, so append the source basename.
Chris@18 486 $destination = file_stream_wrapper_uri_normalize($destination . '/' . $this->basename($source));
Chris@18 487 }
Chris@18 488 else {
Chris@18 489 // Perhaps $destination is a dir/file?
Chris@18 490 $dirname = $this->dirname($destination);
Chris@18 491 if (!$this->prepareDirectory($dirname)) {
Chris@18 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@18 493 '%original_source' => $original_source,
Chris@18 494 ]);
Chris@18 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@18 496 }
Chris@18 497 }
Chris@18 498
Chris@18 499 // Determine whether we can perform this operation based on overwrite rules.
Chris@18 500 $destination = $this->getDestinationFilename($destination, $replace);
Chris@18 501 if ($destination === FALSE) {
Chris@18 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@18 503 '%original_source' => $original_source,
Chris@18 504 '%destination' => $destination,
Chris@18 505 ]);
Chris@18 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@18 507 }
Chris@18 508
Chris@18 509 // Assert that the source and destination filenames are not the same.
Chris@18 510 $real_source = $this->realpath($source);
Chris@18 511 $real_destination = $this->realpath($destination);
Chris@18 512 if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
Chris@18 513 $this->logger->error("File '%source' could not be copied because it would overwrite itself.", [
Chris@18 514 '%source' => $source,
Chris@18 515 ]);
Chris@18 516 throw new FileException("File '$source' could not be copied because it would overwrite itself.");
Chris@18 517 }
Chris@18 518 // Make sure the .htaccess files are present.
Chris@18 519 // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304.
Chris@18 520 file_ensure_htaccess();
Chris@18 521 }
Chris@18 522
Chris@18 523 /**
Chris@18 524 * {@inheritdoc}
Chris@18 525 */
Chris@18 526 public function saveData($data, $destination, $replace = self::EXISTS_RENAME) {
Chris@18 527 // Write the data to a temporary file.
Chris@18 528 $temp_name = $this->tempnam('temporary://', 'file');
Chris@18 529 if (file_put_contents($temp_name, $data) === FALSE) {
Chris@18 530 $this->logger->error("Temporary file '%temp_name' could not be created.", ['%temp_name' => $temp_name]);
Chris@18 531 throw new FileWriteException("Temporary file '$temp_name' could not be created.");
Chris@18 532 }
Chris@18 533
Chris@18 534 // Move the file to its final destination.
Chris@18 535 return $this->move($temp_name, $destination, $replace);
Chris@18 536 }
Chris@18 537
Chris@18 538 /**
Chris@18 539 * {@inheritdoc}
Chris@18 540 */
Chris@18 541 public function prepareDirectory(&$directory, $options = self::MODIFY_PERMISSIONS) {
Chris@18 542 if (!$this->validScheme($this->uriScheme($directory))) {
Chris@18 543 // Only trim if we're not dealing with a stream.
Chris@18 544 $directory = rtrim($directory, '/\\');
Chris@18 545 }
Chris@18 546
Chris@18 547 // Check if directory exists.
Chris@18 548 if (!is_dir($directory)) {
Chris@18 549 // Let mkdir() recursively create directories and use the default
Chris@18 550 // directory permissions.
Chris@18 551 if ($options & static::CREATE_DIRECTORY) {
Chris@18 552 return @$this->mkdir($directory, NULL, TRUE);
Chris@18 553 }
Chris@18 554 return FALSE;
Chris@18 555 }
Chris@18 556 // The directory exists, so check to see if it is writable.
Chris@18 557 $writable = is_writable($directory);
Chris@18 558 if (!$writable && ($options & static::MODIFY_PERMISSIONS)) {
Chris@18 559 return $this->chmod($directory);
Chris@18 560 }
Chris@18 561
Chris@18 562 return $writable;
Chris@18 563 }
Chris@18 564
Chris@18 565 /**
Chris@18 566 * {@inheritdoc}
Chris@18 567 */
Chris@18 568 public function getDestinationFilename($destination, $replace) {
Chris@18 569 $basename = $this->basename($destination);
Chris@18 570 if (!Unicode::validateUtf8($basename)) {
Chris@18 571 throw new FileException(sprintf("Invalid filename '%s'", $basename));
Chris@18 572 }
Chris@18 573 if (file_exists($destination)) {
Chris@18 574 switch ($replace) {
Chris@18 575 case FileSystemInterface::EXISTS_REPLACE:
Chris@18 576 // Do nothing here, we want to overwrite the existing file.
Chris@18 577 break;
Chris@18 578
Chris@18 579 case FileSystemInterface::EXISTS_RENAME:
Chris@18 580 $directory = $this->dirname($destination);
Chris@18 581 $destination = $this->createFilename($basename, $directory);
Chris@18 582 break;
Chris@18 583
Chris@18 584 case FileSystemInterface::EXISTS_ERROR:
Chris@18 585 // Error reporting handled by calling function.
Chris@18 586 return FALSE;
Chris@18 587 }
Chris@18 588 }
Chris@18 589 return $destination;
Chris@18 590 }
Chris@18 591
Chris@18 592 /**
Chris@18 593 * {@inheritdoc}
Chris@18 594 */
Chris@18 595 public function createFilename($basename, $directory) {
Chris@18 596 $original = $basename;
Chris@18 597 // Strip control characters (ASCII value < 32). Though these are allowed in
Chris@18 598 // some filesystems, not many applications handle them well.
Chris@18 599 $basename = preg_replace('/[\x00-\x1F]/u', '_', $basename);
Chris@18 600 if (preg_last_error() !== PREG_NO_ERROR) {
Chris@18 601 throw new FileException(sprintf("Invalid filename '%s'", $original));
Chris@18 602 }
Chris@18 603 if (substr(PHP_OS, 0, 3) == 'WIN') {
Chris@18 604 // These characters are not allowed in Windows filenames.
Chris@18 605 $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
Chris@18 606 }
Chris@18 607
Chris@18 608 // A URI or path may already have a trailing slash or look like "public://".
Chris@18 609 if (substr($directory, -1) == '/') {
Chris@18 610 $separator = '';
Chris@18 611 }
Chris@18 612 else {
Chris@18 613 $separator = '/';
Chris@18 614 }
Chris@18 615
Chris@18 616 $destination = $directory . $separator . $basename;
Chris@18 617
Chris@18 618 if (file_exists($destination)) {
Chris@18 619 // Destination file already exists, generate an alternative.
Chris@18 620 $pos = strrpos($basename, '.');
Chris@18 621 if ($pos !== FALSE) {
Chris@18 622 $name = substr($basename, 0, $pos);
Chris@18 623 $ext = substr($basename, $pos);
Chris@18 624 }
Chris@18 625 else {
Chris@18 626 $name = $basename;
Chris@18 627 $ext = '';
Chris@18 628 }
Chris@18 629
Chris@18 630 $counter = 0;
Chris@18 631 do {
Chris@18 632 $destination = $directory . $separator . $name . '_' . $counter++ . $ext;
Chris@18 633 } while (file_exists($destination));
Chris@18 634 }
Chris@18 635
Chris@18 636 return $destination;
Chris@18 637 }
Chris@18 638
Chris@0 639 }