diff -u b/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php --- b/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -7,6 +7,7 @@ namespace Drupal\file\Entity; +use Drupal\Component\Utility\Crypt; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -194,7 +195,10 @@ public function preSave(EntityStorageInterface $storage) { parent::preSave($storage); - $this->setSize(filesize($this->getFileUri())); + $uri = $this->getFileUri(); + $this->setSize(filesize($uri)); + // Save the hash of the URI so we can enforce uniqueness. + $this->get('uri_hash')->value = Crypt::hashBase64($uri); } /** @@ -249,10 +253,17 @@ $fields['uri'] = BaseFieldDefinition::create('uri') ->setLabel(t('URI')) ->setDescription(t('The URI to access the file (either local or remote).')) - ->setSetting('is_ascii', TRUE) ->setSetting('max_length', 255) ->setSetting('case_sensitive', TRUE); + $fields['uri_hash'] = BaseFieldDefinition::create('string') + ->setLabel(t('URI Hash')) + ->setDescription(t('The hashed URI. Saved to enforce uniqueness.')) + ->setSetting('is_ascii', TRUE) + // As this is a base64 encoded sha256 hash, 128 ought to be enough. + ->setSetting('max_length', 128) + ->setSetting('case_sensitive', TRUE); + $fields['filemime'] = BaseFieldDefinition::create('string') ->setLabel(t('File MIME type')) ->setSetting('is_ascii', TRUE) reverted: --- b/core/modules/file/src/FileStorage.php +++ a/core/modules/file/src/FileStorage.php @@ -8,7 +8,6 @@ namespace Drupal\file; use Drupal\Core\Entity\Sql\SqlContentEntityStorage; -use Drupal\Core\Entity\EntityInterface; /** * File storage for files. @@ -18,39 +17,6 @@ /** * {@inheritdoc} */ - protected function doLoadMultiple(array $ids = NULL) { - $entities = parent::doLoadMultiple($ids); - foreach ($entities as $entity) { - // @see FileStorage::doSave() - $entity->setFileUri($this->uriFromAscii($entity->getFileUri())); - } - } - - /** - * {@inheritdoc} - */ - public function loadByProperties(array $values = array()) { - foreach ($values as $name => $value) { - if ($name == 'uri') { - // @see FileStorage::doSave() - $values[$name] = $this->uriToAscii($value); - } - } - parent::loadByProperties($values); - } - - /** - * {@inheritdoc} - */ - public function doSave($id, EntityInterface $entity) { - $entity->setFileUri($this->uriToAscii($entity->getFileUri())); - parent::doSave($id, $entity); - $entity->setFileUri($this->uriFromAscii($entity->getFileUri())); - } - - /** - * {@inheritdoc} - */ public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) { $query = $this->database->select($this->entityType->getBaseTable(), 'f') ->condition('f.status', $status); @@ -61,33 +27,4 @@ return $query->execute()->fetchField(); } - /** - * Turns a URI string into an ASCII encoded string. - * - * This will be used to save non-ASCII data into the URI field, which is - * marked as an ASCII field so it can contain a unique constraint and - * be 255 characters long at the same time. - * - * @param string $url - * Provided URI, possibly non-ASCII. - * - * @return string - * ASCII-encoded URI. - */ - protected function uriToAscii($url) { - return str_replace('%3A', ':', str_replace('%2F', '/', rawurlencode($url))); - } - - /** - * Inverse method of FileStorage::urlToAscii(). - * - * @param string $url - * ASCII encoded URI, from database. - * - * @return string - * Originally provided URI. - */ - protected function uriFromAscii($url) { - return rawurldecode($url); - } } only in patch2: unchanged: --- a/core/modules/file/src/FileStorageSchema.php +++ b/core/modules/file/src/FileStorageSchema.php @@ -30,6 +30,9 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st break; case 'uri': + $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE); + break; + case 'uri_hash': $this->addSharedTableFieldUniqueKey($storage_definition, $schema, TRUE); break; } only in patch2: unchanged: --- a/core/modules/file/src/Tests/SaveTest.php +++ b/core/modules/file/src/Tests/SaveTest.php @@ -8,6 +8,9 @@ namespace Drupal\file\Tests; use Drupal\file\Entity\File; +use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; /** * File saving tests. @@ -59,7 +62,7 @@ function testFileSave() { // Try to insert a second file with the same name apart from case insensitivity // to ensure the 'uri' index allows for filenames with different cases. - $uppercase_file = File::create(array( + $uppercase_values = array( 'uid' => 1, 'filename' => 'DRUPLICON.txt', 'uri' => 'public://DRUPLICON.txt', @@ -67,10 +70,25 @@ function testFileSave() { 'created' => 1, 'changed' => 1, 'status' => FILE_STATUS_PERMANENT, - )); + ); + $uppercase_file = File::create($uppercase_values); file_put_contents($uppercase_file->getFileUri(), 'hello world'); $uppercase_file->save(); + $storage = \Drupal::entityManager()->getStorage('file'); + $this->assertTrue($storage instanceof EntityStorageInterface, 'Storage object implements EntityStorageInterface.'); + if ($storage instanceof SqlEntityStorageInterface) { + // Ensure the database URL uniqueness constraint is triggered. + $uppercase_file_duplicate = File::create($uppercase_values); + file_put_contents($uppercase_file_duplicate->getFileUri(), 'hello world'); + try { + $uppercase_file_duplicate->save(); + } catch (EntityStorageException $e) { + $exception_triggered = (0 === strpos($e->getCode(), '23')); + } + $this->assertTrue($exception_triggered, 'SQL uniqueness constraint is triggered'); + } + // Ensure that file URI entity queries are case sensitive. $fids = \Drupal::entityQuery('file') ->condition('uri', $uppercase_file->getFileUri()) only in patch2: unchanged: --- a/core/modules/migrate_drupal/src/Tests/d6/MigrateFileTest.php +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateFileTest.php @@ -8,6 +8,7 @@ namespace Drupal\migrate_drupal\Tests\d6; use Drupal\Component\Utility\Random; +use Drupal\Component\Utility\Crypt; use Drupal\migrate\MigrateExecutable; use Drupal\migrate\Tests\MigrateDumpAlterInterface; use Drupal\migrate_drupal\Tests\d6\MigrateDrupal6TestBase; @@ -63,6 +64,7 @@ public function testFiles() { $this->assertIdentical('Image1.png', $file->getFilename()); $this->assertIdentical('39325', $file->getSize()); $this->assertIdentical('public://image-1.png', $file->getFileUri()); + $this->assertIdentical(Crypt::hashBase64('public://image-1.png'), $file->get('uri_hash')->value); $this->assertIdentical('image/png', $file->getMimeType()); // It is pointless to run the second half from MigrateDrupal6Test. if (empty($this->standalone)) {