Problem/Motivation

When adding support for temporary files to be migrated we assumed the temporary path to be absolute because it was set to "/tmp" in the dumps. This is a wrong assumption.

Proposed resolution

* We need to remove these assumptions from the EntityFile destination.
* Update the FileUri process plugin.
* Make sure the source sets the correct data on the row so we have source_base_path in the destination.

Remaining tasks

Write patch and tests.

User interface changes

n/a

API changes

n/a

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

benjy’s picture

Status: Active » Postponed

Postponing on #2414365: File migration fails when the files are at a remote URI which introduces some unit tests for the file destination.

benjy’s picture

FileSize
2.58 KB
1.02 KB
Anonymous’s picture

Status: Postponed » Needs review

I guess this is no longer postponed since #2414365: File migration fails when the files are at a remote URI made it in. Let's first see what the testbot thinks of this.

Status: Needs review » Needs work

The last submitted patch, 2: 2417975-FAIL.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
5.83 KB
3.6 KB

At first glance, the patch looks good to me (and testbot has been passing the one that isn't marked "FAIL"...hmmm). Here it is again with a few relatively minor modifications (removed deprecated file-related functions calls in favor of injecting the file_system service).

Status: Needs review » Needs work

The last submitted patch, 5: 2417975-5.patch, failed testing.

phenaproxima’s picture

EntityFileTest should now pass.

I added a not-very-elegant kludge to EntityFile::import() to work around a quirk of file_prepare_directory(). If FileSystemInterface::dirname() returns a scheme -- for example, passing public://foobaz.txt will return public:// --, file_prepare_directory() will reject that URI and EntityFile will throw a MigrateException.

The fix checks the output of FileSystemInterface::dirname() against a regex and runs it through FileSystemInterface::realpath() if it's a scheme without a path, before file_prepare_directory() is called.

phenaproxima’s picture

Status: Needs work » Needs review

Updating status.

phenaproxima’s picture

FileSize
7.16 KB
757 bytes

Changed preg_match() call to substr().

The last submitted patch, 7: 2417975-7.patch, failed testing.

benjy’s picture

Can we get a fail patch to sure the new test works?

I'll have to think about the changes in the destination in regards to the dirname() etc.

phenaproxima’s picture

FileSize
6.6 KB

Sure thing. This patch should fail EntityFileTest with an exception like:

Drupal\migrate\MigrateException: Could not create directory <em class="placeholder">public://</em> in Drupal\migrate\Plugin\migrate\destination\EntityFile->import() (line 76 of /path/to/core/modules/migrate/src/Plugin/migrate/destination/EntityFile.php).

phenaproxima’s picture

...and yet somehow it didn't fail. Strange; it fails quite nicely in my local environment.

phenaproxima’s picture

Some of @chx's work in #2482329: Remove deprecated calls from EntityFile and make it less local overlaps with the work in #9 -- namely, the removal of deprecated calls and injection of the file_system service. I'm thinking that issue should be postponed until this patch is committed closed, and its fixes integrated into this patch.

phenaproxima’s picture

This patch smoothes over EntityFile's handling of remote streams by checking if the source and destination URIs are both local. I didn't add any specific test cases for this, because a) I'm not sure what exactly how to go about testing this, and b) there are no remote stream wrappers bundled with core that don't extend LocalStream (which made me throw a WtfException on IRC).

Status: Needs review » Needs work

The last submitted patch, 15: 2417975-15.patch, failed testing.

phenaproxima queued 15: 2417975-15.patch for re-testing.

The last submitted patch, 15: 2417975-15.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
10.09 KB
3.58 KB

After refactoring EntityFile, this passes the tests on my localhost.

phenaproxima’s picture

Did a bit of minor refactoring requested by @chx after he reviewed.

phenaproxima’s picture

Regarding a fail patch -- I'm not sure we need one, because it appears to be flat-out broken in HEAD. On the latest 8.0.x (e0aae8c), MigrateFileTest will fail epically with a fatal error, and EntityFileTest will fail with a warning and a MigrateException ("cannot create directory public://").

After applying the patch in #20, both tests pass with flying colors.

phenaproxima’s picture

I <3 dependency injection. Updated the patch to inject the string_translation service into EntityFile.

phenaproxima’s picture

This integrates the changes from #2483939: EntityFile should throw an exception if the source file doesn't exist -- it ensures the source file exists and throws an exception if it doesn't.

phenaproxima’s picture

At @chx's suggestion, this patch refactors EntityFile so that file_prepare_directory() is only called if needed -- that is, if an initial attempt to save the destination file fails.

chx’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
12.75 KB
1.13 KB

This looks awesome. Thanks much. I made two very minor changes please do NOT credit me: one empty() was not necessary and the only change in FileUri was whitespace so I reverted the file.

benjy’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFile.php
    @@ -36,6 +50,29 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    +      array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
    

    Can we not calculate this in the constructor?

  2. +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFile.php
    @@ -44,52 +81,141 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    +      throw new MigrateException($this->t('File %source does not exist.', ['%source' => $source]));
    

    We don't translate exception messages.

  3. +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFile.php
    @@ -44,52 +81,141 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    +        throw new MigrateException($this->t('Could not create directory %dir', ['%dir' => $dir]));
    

    same here.

  4. +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFile.php
    @@ -44,52 +81,141 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    +      throw new MigrateException($this->t('File %source could not be copied to %destination.', ['%source' => $source, '%destination' => $destination]));
    

    and again.

Also, I do wonder if we need some unit tests for some of those new methods in EntityFile such as isLocalUri(), isLocationUnchanged(), getDirectory()

chx’s picture

> Can we not calculate this in the constructor?

When testing, you'd need to mock bundleinfo for no benefit so ... no. The rest, agreed.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
14.82 KB
6.49 KB

Removed translation from the exceptions.

Regarding unit tests for the helper methods -- I'm fully up for it, if it's not too much of a pain (or, for that matter, considered unkosher) to forcibly change those protected methods to public ones for testing.

benjy’s picture

When testing, you'd need to mock bundleinfo for no benefit so ... no.

No problem, I was just trying to reduce the number of dependencies EntityFile has.

change those protected methods to public ones for testing.

I don't see any problems with that, i've done it myself elsewhere.

  1. +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateFileTest.php
    @@ -92,11 +98,21 @@ public function testFiles() {
    +    if ($file instanceof FileInterface) {
    +      $this->assertIdentical('public://core/modules/simpletest/files/image-2.jpg', $file->getFileUri());
    +    }
    +    else {
    +      $this->fail('File 2 does not exist.');
    +    }
    

    This is interesting, why not just do the assertion?

  2. +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateFileTest.php
    @@ -92,11 +98,21 @@ public function testFiles() {
    +    if ($file instanceof FileInterface) {
    

    Same here and i think one other place.

phenaproxima’s picture

I added the instanceof assertions because, if the file isn't loaded, the test will try to access a non-object and thus blow up with a fatal error, which is annoying as hell. Adding a fail saves us a trip to the "Clean environment" button. :)

benjy’s picture

Personally I think we should remove them, i've not seen this pattern in core before?

chx’s picture

I agree with benjy here.

phenaproxima’s picture

Removed the instanceof checks from MigrateFileTest, and added unit tests of EntityFile's protected helpers.

neclimdul’s picture

Status: Needs review » Needs work

Awesome work, this fixes a huge number of errors on my test import.

  1. +++ b/core/scripts/run-tests.sh
    @@ -1,3 +1,4 @@
    +#!/usr/bin/env php
    

    I agree but we can't do that here. :)

  2. +++ b/core/modules/migrate/tests/src/Unit/destination/EntityFileTest.php
    @@ -39,8 +40,10 @@ class EntityFileTest extends KernelTestBase {
    +    // $this->installConfig(['system', 'file']);
    

    Just remove this.

  3. +++ b/core/modules/migrate/tests/src/Unit/destination/EntityFileTest.php
    @@ -157,4 +278,39 @@ protected function getEntity(Row $row, array $old_destination_id_values) {
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function writeFile($source, $destination, $replace = FILE_EXISTS_REPLACE) {
    +    return parent::writeFile($source, $destination, $replace);
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function getOverwriteMode(Row $row) {
    +    return parent::getOverwriteMode($row);
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function getDirectory($uri) {
    +    return parent::getDirectory($uri);
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function isLocationUnchanged($source, $destination) {
    +    return parent::isLocationUnchanged($source, $destination);
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function isLocalUri($uri) {
    +    return parent::isLocalUri($uri);
    +  }
    +
    

    Why?

  4. +++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateFileTest.php
    @@ -8,6 +8,7 @@
    +use Drupal\file\FileInterface;
    
    @@ -64,6 +65,7 @@ public function testFiles() {
    +
    

    ?

phenaproxima’s picture

Regarding #3 -- it was requested to have unit tests of those helper methods, which are all protected in EntityFile. So I had to override them with public versions in order to test. Everything else, is valid, and I will re-roll the patch today.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
19.55 KB
1.32 KB

All fixed, except for #3.

neclimdul’s picture

Status: Needs review » Reviewed & tested by the community

Did another review and looks fine. Definitely fixes a lot of failures in my test site migration.

benjy’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFile.php
    @@ -44,52 +79,141 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    +   * @return false|string
    

    boolean|string

  2. +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFile.php
    @@ -44,52 +79,141 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    +   * @param string $destination
    +   *  The destination URI.
    +   *
    +   * @return boolean
    +   */
    

    Missing return description.

  3. +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFile.php
    @@ -44,52 +79,141 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    +   * @param string $uri
    +   *  The URI or path to test.
    +   *
    +   * @return boolean
    

    same here

  4. +++ b/core/modules/migrate/tests/src/Unit/destination/EntityFileTest.php
    @@ -7,26 +7,27 @@
    +class EntityFileTest extends WebTestBase {
    

    Huh? This test is in tests/src/Unit, we can't just make it a web test?

  5. +++ b/core/modules/migrate/tests/src/Unit/destination/EntityFileTest.php
    @@ -77,17 +81,118 @@ protected function localFileDataProvider() {
    +      $this->assertIdentical($e->getMessage(), 'File <em class="placeholder">' . $destination . '</em> does not exist.');
    

    It seems funny to me to have html in exception messages, can't we just use @ instead if we're using SafeMarkup::format?

Normally, testing protected/private methods directly isn't usually recommended but I do think it adds value in this case. Regarding #3, you could have done it like so if you prefer:

    $$method = new \ReflectionMethod($entity_file, 'isLocalUri');
    $method->setAccessible(TRUE);
    $method->invoke($uri)
phenaproxima’s picture

Status: Needs work » Needs review
FileSize
26.2 KB
22.73 KB

All fixed.

@benjy, I prefer the idea of using reflection to alter the method accessibility, so that's what I did in this patch. I also changed EntityFileTest to a KernelTestBase (turns out it doesn't really need to be a web test, thank Cthulhu) and moved it out of the unit test suite.

benjy’s picture

  1. +++ b/core/modules/migrate/src/Tests/EntityFileTest.php
    @@ -0,0 +1,295 @@
    +    $this->assertTrue($method->invoke($plugin, 'public://foo.txt'));
    +    $this->assertTrue($method->invoke($plugin, 'public://path/to/foo.txt'));
    +    $this->assertTrue($method->invoke($plugin, 'temporary://foo.txt'));
    +    $this->assertTrue($method->invoke($plugin, 'temporary://path/to/foo.txt'));
    +    $this->assertTrue($method->invoke($plugin, 'foo.txt'));
    +    $this->assertTrue($method->invoke($plugin, '/path/to/files/foo.txt'));
    +    $this->assertTrue($method->invoke($plugin, 'relative/path/to/foo.txt'));
    +    $this->assertFalse($method->invoke($plugin, 'http://www.example.com/foo.txt'));
    

    Why not use the data provider pattern here like we did above in testSuccessfulCopies().

    I guess that was left over from the PHPUnit test, we should at least be consistent?

  2. +++ b/core/modules/migrate/src/Tests/EntityFileTest.php
    @@ -0,0 +1,295 @@
    +class TestEntityFile extends EntityFile {
    

    Shame we need this, once #2304461: KernelTestBaseTNG™ lands, we should be able to get rid of it.

benjy’s picture

Status: Needs review » Reviewed & tested by the community

OK, lets do it. The test improvements can always be a follow-up once KernelTestBaseNG lands.

phenaproxima’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
26.2 KB

Quick re-roll.

benjy’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 42: 2417975-41.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Needs review

*shakes fist at testbot*

phenaproxima queued 42: 2417975-41.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 42: 2417975-41.patch, failed testing.

Status: Needs work » Needs review

phenaproxima queued 42: 2417975-41.patch for re-testing.

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC after testbot tantrum.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 6ac8e46 and pushed to 8.0.x. Thanks!

diff --git a/core/modules/migrate/src/Tests/EntityFileTest.php b/core/modules/migrate/src/Tests/EntityFileTest.php
index 2fb4b62..509e98b 100644
--- a/core/modules/migrate/src/Tests/EntityFileTest.php
+++ b/core/modules/migrate/src/Tests/EntityFileTest.php
@@ -112,7 +112,7 @@ public function testWriteFile() {
     $plugin->configuration['move'] = TRUE;
     $this->assertTrue($method->invoke($plugin, 'temporary://baz.txt', 'public://foo.txt'));
 
-    // Trying to move a non-existent file shold return FALSE.
+    // Trying to move a non-existent file should return FALSE.
     $this->assertFalse($method->invoke($plugin, 'temporary://invalid.txt', 'public://invalid.txt'));
 
     // Copying over a file that already exists should replace the existing file.
@@ -193,7 +193,7 @@ public function testIsLocationUnchanged() {
   }
 
   /**
-   * Tests various inovcations of the isLocalUri() method.
+   * Tests various invocations of the isLocalUri() method.
    */
   public function testIsLocalUri() {
     $plugin = $this->destination;

Fixed minor spelling mistakes on commit.

  • alexpott committed 6ac8e46 on 8.0.x
    Issue #2417975 by phenaproxima, benjy, chx, neclimdul: EntityFile...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.