diff --git a/core/modules/file/file.module b/core/modules/file/file.module index d98b0f6f..9dbc885e 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -177,25 +177,21 @@ function file_copy(FileInterface $source, $destination = NULL, $replace = FileSy try { $uri = $file_system->copy($source->getFileUri(), $destination, $replace); - $file = $source->createDuplicate(); - $file->setFileUri($uri); - $file->setFilename($file_system->basename($uri)); - // If we are replacing an existing file re-use its database record. - // @todo Do not create a new entity in order to update it. See - // https://www.drupal.org/node/2241865. - if ($replace == FileSystemInterface::EXISTS_REPLACE) { - $existing_files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]); - if (count($existing_files)) { - $existing = reset($existing_files); - $file->fid = $existing->id(); - $file->setOriginalId($existing->id()); - $file->setFilename($existing->getFilename()); - } + // If we are replacing an existing file, load it. + if ($replace == FileSystemInterface::EXISTS_REPLACE && $existing_files = entity_load_multiple_by_properties('file', ['uri' => $uri])) { + $file = reset($existing_files); } - // If we are renaming around an existing file (rather than a directory), - // use its basename for the filename. - elseif ($replace == FileSystemInterface::EXISTS_RENAME && is_file($destination)) { - $file->setFilename($file_system->basename($destination)); + else { + $file = $source->createDuplicate(); + $file->setFileUri($uri); + // If we are renaming around an existing file (rather than a directory), + // use its basename for the filename. + if ($replace === FileSystemInterface::EXISTS_RENAME && is_file($destination)) { + $file->setFilename($file_system->basename($destination)); + } + else { + $file->setFilename($file_system->basename($uri)); + } } $file->save(); @@ -584,29 +580,22 @@ function file_save_data($data, $destination = NULL, $replace = FileSystemInterfa try { $uri = \Drupal::service('file_system')->saveData($data, $destination, $replace); - // Create a file entity. - $file = File::create([ - 'uri' => $uri, - 'uid' => $user->id(), - 'status' => FILE_STATUS_PERMANENT, - ]); - // If we are replacing an existing file re-use its database record. - // @todo Do not create a new entity in order to update it. See - // https://www.drupal.org/node/2241865. - if ($replace == FileSystemInterface::EXISTS_REPLACE) { - $existing_files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]); - if (count($existing_files)) { - $existing = reset($existing_files); - $file->fid = $existing->id(); - $file->setOriginalId($existing->id()); - $file->setFilename($existing->getFilename()); - } - } - // If we are renaming around an existing file (rather than a directory), - // use its basename for the filename. - elseif ($replace == FileSystemInterface::EXISTS_RENAME && is_file($destination)) { - $file->setFilename(\Drupal::service('file_system')->basename($destination)); + // If we are replacing an existing file, load it. + if ($replace == FileSystemInterface::EXISTS_REPLACE && $existing_files = entity_load_multiple_by_properties('file', ['uri' => $uri])) { + $file = reset($existing_files); + $file->setPermanent(); } + else { + // Create a file entity. + $file = File::create(['uri' => $uri]); + $file->setOwnerId($user->id()); + $file->setPermanent(); + // If we are renaming around an existing file (rather than a directory), + // use its basename for the filename. + if ($replace == FileSystemInterface::EXISTS_RENAME && is_file($destination)) { + $file->setFilename(\Drupal::service('file_system')->basename($destination)); + } + } $file->save(); return $file; diff --git a/core/modules/file/tests/file_test/file_test.module b/core/modules/file/tests/file_test/file_test.module index e31547a6..df61ce69 100644 --- a/core/modules/file/tests/file_test/file_test.module +++ b/core/modules/file/tests/file_test/file_test.module @@ -359,3 +359,10 @@ function file_test_entity_type_alter(&$entity_types) { ->setAccessClass('Drupal\file_test\FileTestAccessControlHandler'); } } + +/** + * Implements hook_ENTITY_TYPE_create() for file entities. + */ +function file_test_file_create() { + \Drupal::state()->set('file_test.hook_entity_create', TRUE); +} diff --git a/core/modules/file/tests/src/Kernel/CopyTest.php b/core/modules/file/tests/src/Kernel/CopyTest.php index f5093508..fd6c951c 100644 --- a/core/modules/file/tests/src/Kernel/CopyTest.php +++ b/core/modules/file/tests/src/Kernel/CopyTest.php @@ -81,6 +81,43 @@ public function testExistingRename() { $this->assertDifferentFile($loaded_source, $loaded_result); } + /** + * Test replacement when copying over a file that does not already exist. + */ + public function testNonExistingReplace() { + // Set up a file to copy. + $contents = $this->randomMachineName(10); + $source = $this->createFile(NULL, $contents); + $target = 'public://testing.dat'; + + // Clone the object so we don't have to worry about the function changing + // our reference copy. + $result = file_copy(clone $source, $target, FileSystemInterface::EXISTS_REPLACE); + + // Check the return status and that the contents changed. + $this->assertTrue($result, 'File copied successfully.'); + $this->assertSame($contents, file_get_contents($result->getFileUri()), 'Contents of file were overwritten.'); + $this->assertDifferentFile($source, $result); + + // Check that the correct hooks were called. + $this->assertFileHooksCalled(['copy', 'insert']); + + // Load all the affected files to check the changes that actually made it + // to the database. + $loaded_source = File::load($source->id()); + $loaded_result = File::load($result->id()); + + // Check that the created file has the correct URI. + $this->assertSame($target, $result->getFileUri()); + $this->assertSame($loaded_result->getFileUri(), $result->getFileUri()); + + // Verify that the source file wasn't changed. + $this->assertFileUnchanged($source, $loaded_source); + + // Verify that what was returned is what's in the database. + $this->assertFileUnchanged($result, $loaded_result); + } + /** * Test replacement when copying over a file that already exists. */ diff --git a/core/modules/file/tests/src/Kernel/SaveDataTest.php b/core/modules/file/tests/src/Kernel/SaveDataTest.php index 38a53c60..07406070 100644 --- a/core/modules/file/tests/src/Kernel/SaveDataTest.php +++ b/core/modules/file/tests/src/Kernel/SaveDataTest.php @@ -94,6 +94,31 @@ public function testExistingRename() { $this->assertFileUnchanged($result, File::load($result->id())); } + /** + * Test file_save_data() when replacing an non-existing file. + */ + public function testNonExistingReplace() { + $contents = $this->randomMachineName(8); + + \Drupal::state()->set('file_test.hook_entity_create', FALSE); + $result = file_save_data($contents, NULL, FileSystemInterface::EXISTS_REPLACE); + $this->assertTrue($result, 'File saved successfully.'); + + $entity_create_hook_fired = \Drupal::state()->get('file_test.hook_entity_create'); + $this->assertTrue($entity_create_hook_fired, 'Entity create hook was invoked for a file replace operation.'); + + $this->assertSame('public', file_uri_scheme($result->getFileUri()), "File was placed in Drupal's files directory."); + $this->assertSame($contents, file_get_contents($result->getFileUri()), 'Contents of the file are correct.'); + $this->assertSame($result->getMimeType(), 'application/octet-stream', 'A MIME type was set.'); + $this->assertTrue($result->isPermanent(), "The file's status was set to permanent."); + + // Check that the correct hooks were called. + $this->assertFileHooksCalled(['insert']); + + // Verify that what was returned is what's in the database. + $this->assertFileUnchanged($result, File::load($result->id())); + } + /** * Test file_save_data() when replacing an existing file. */ @@ -102,9 +127,13 @@ public function testExistingReplace() { $existing = $this->createFile(); $contents = $this->randomMachineName(8); + \Drupal::state()->set('file_test.hook_entity_create', FALSE); $result = file_save_data($contents, $existing->getFileUri(), FileSystemInterface::EXISTS_REPLACE); $this->assertNotFalse($result, 'File saved successfully.'); + $entity_create_hook_fired = \Drupal::state()->get('file_test.hook_entity_create'); + $this->assertFalse($entity_create_hook_fired, 'Entity create hook was not invoked for a file replace operation.'); + $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager'); assert($stream_wrapper_manager instanceof StreamWrapperManagerInterface); $this->assertEqual('public', $stream_wrapper_manager::getScheme($result->getFileUri()), "File was placed in Drupal's files directory.");