Lately I’ve been working on a large migration project where thousands of files had to be moved into Drupal as file entities. The source files were stored on Amazon S3, and with the help of the s3fs module and Drupal’s Migration API, it was straightforward to write a functioning migration.
However, there was one important quirk worth sharing—something you might run into if you’re doing a similar migration.
Rollbacks and File Deletion in Drupal
Drupal’s Migration API supports the full lifecycle of entities:
- create – migrate new entities,
- update – re-run migrations to refresh data,
- delete (rollback) – remove entities that were created by a migration.
When rolling back file entities, Drupal doesn’t just delete the database record. It also unlinks the actual file from the filesystem.
For local file storage, this is a great feature: rolling back means your database and filesystem stay in sync.
But with S3, that behavior is problematic. Files on S3 often need to persist, since they might be referenced outside of Drupal by other systems or services. Accidentally unlinking them during a rollback is not acceptable.
First Attempt: Predelete Hook
My first idea was to prevent deletion at the entity level by throwing an exception from a hook_ENTITY_TYPE_predelete().
Unfortunately, that doesn’t work in this case. Drupal actually unlinks the file before the predelete hook is dispatched. By the time the hook is called, it’s already too late—the file has been removed.
The Real Fix: Custom Destination Plugin
The solution was to extend the EntityFile destination plugin that Drupal provides. By overriding the part of the process that removes files, I could stop Drupal from unlinking anything while still allowing the rollback of file entities themselves.
Here’s the custom plugin class I ended up with:
```
<?php
namespace Drupal\mymodule\Plugin\migrate\destination;
use Drupal\file\Plugin\migrate\destination\EntityFile;
use Drupal\migrate\Attribute\MigrateDestination;
/**
* Provides a migration destination plugin for File entities.
*/
#[MigrateDestination('mymodule:file')]
class EntityFileNoRollback extends EntityFile {
/**
* {@inheritdoc}
*
* Files using this destination plugin will not be rolled back even if they
* appear as they were rolled back.
*/
public function rollback(array $destination_identifier) {
// Do nothing.
}
}
```
With this in place, I just updated my migration YAML to use the new destination plugin:
```
destination:
plugin: mymodule:file
```