How to migrate files and images to a media field in Drupal

Client
  • Omitsis
Technologies
Services
Date
  • 11/01/2026

When migrating content to Drupal from another source (CSV, Drupal 7, external systems, etc.), it’s very common to deal with images and files. Since Drupal 8 and especially in Drupal 9–11, Media entities are the recommended way to handle images and files instead of using image/file fields directly.

This article explains how to migrate images into Media entities using Migrate, step by step, and then reference those Media entities from content. The examples are based on a CSV source, but the same concepts apply to other sources.

Why use Media instead of file/image fields?

The Media module has been in Drupal core for years now and is mature and stable in Drupal 10 and Drupal 11. Using Media entities gives you:

  • Reusable images and files across content
  • A centralized Media Library
  • Extra fields on media (copyright, author, alt defaults, etc.)
  • Better editorial experience

At Omitsis, using Media is now, and for years, a default standard for Drupal projects.

The downside is that migrating to Media is slightly more complex than migrating directly to file fields. This post focuses on solving that.

Prerequisites

This article assumes basic knowledge of the Migrate API.

If you’re new to migrations, it’s recommended to first read an introduction on migrating from CSV using the Migrate module.

We will not use contrib helpers like migrate_files_to_media. Instead, we’ll rely only on core/contrib migrate plugins so you fully understand what’s happening.

Required modules (Drupal 10 / 11)

Install the required modules via Composer:

composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_source_csv drupal/config_devel

Enable them:

drush en -y migrate migrate_plus migrate_tools migrate_source_csv config_devel

config_devel is optional but very convenient during development to auto-install migration YAML files.

Migration strategy overview

We’ll split the process into three migrations:

  1. Files migration – copy image files into Drupal and create file entities
  2. Media migration – create media:image entities referencing those files
  3. Content migration – create nodes referencing the Media entities

This separation makes migrations easier to debug, rerun and reuse.

1. Migrating image files

First, we migrate the physical image files and create file entities.

Migration file: migrate_plus.migration.migrate_files.yml
id: migrate_files
label: Migrate Files

source:
  plugin: csv
  path: modules/custom/custom_migrate/data/products.csv
  delimiter: ','
  enclosure: '"'
  header_offset: 0
  ids:
    - code
  fields:
    0:
      name: cat
      label: 'Category'
    1:
      name: code
      label: 'Product code'
    2:
      name: title
      label: 'Title'
    3:
      name: image
      label: 'Image'

constants:
  file_source_uri: 'public://import/source/images'
  file_dest_uri: 'public://imports/dest/images'

process:
  file_source:
    - plugin: concat
      delimiter: '/'
      source:
        - constants/file_source_uri
        - image
    - plugin: urlencode

  file_dest:
    - plugin: concat
      delimiter: '/'
      source:
        - constants/file_dest_uri
        - image
    - plugin: urlencode

  uri:
    plugin: file_copy
    source:
      - '@file_source'
      - '@file_dest'

  uid:
    plugin: default_value
    default_value: 1

destination:
  plugin: entity:file

Key points

  • Constants define source and destination directories
  • file_copy copies the image and creates a file entity
  • The CSV only contains the image filename, not full paths
  • file_copy supports overwrite and reuse options if needed

At this point, Drupal knows about the files, but not yet as Media.

2. Creating Media entities from files

Now we convert the files into media:image entities.

Migration file: migrate_plus.migration.media_images.yml
id: media_images
label: Media images

source:
  plugin: csv
  path: modules/custom/custom_migrate/data/products.csv
  delimiter: ','
  enclosure: '"'
  header_offset: 0
  ids:
    - code
  fields:
    0:
      name: cat
      label: 'Category'
    1:
      name: code
      label: 'Product code'
    2:
      name: title
      label: 'Title'
    3:
      name: image
      label: 'Image'

process:
  # Avoid creating duplicate media entities
  skip:
    - plugin: entity_lookup
      entity_type: media
      bundle: image
      value_key: name
      source: image
      ignore_case: true
      access_check: false
    - plugin: skip_on_not_empty
      method: row
      message: 'Skipping already existing media'

  field_media_image/target_id:
    plugin: migration_lookup
    migration: migrate_files
    source: code
    no_stub: true

  uid:
    plugin: default_value
    default_value: 1

destination:
  plugin: entity:media
  default_bundle: image

migration_dependencies:
  optional:
    - migrate_files

Key points

  • Media reuse is critical: one image = one Media entity
  • entity_lookup checks if a Media with the same name already exists
  • migration_lookup retrieves the fid from the previous migration
  • default_bundle is set to image

This approach prevents duplicate media entities when the same image appears multiple times in the CSV.

3. Migrating content and referencing Media

Finally, we migrate the content (nodes) and reference the Media entities.

Migration file: migrate_plus.migration.products.yml
id: products
label: Import products

source:
  plugin: csv
  path: modules/custom/custom_migrate/data/products.csv
  delimiter: ','
  enclosure: '"'
  header_offset: 0
  ids:
    - code
  fields:
    0:
      name: cat
      label: 'Category'
    1:
      name: code
      label: 'Product code'
    2:
      name: title
      label: 'Title'
    3:
      name: image
      label: 'Image'

process:
  field_cat:
    plugin: entity_lookup
    entity_type: taxonomy_term
    bundle: cat_producto
    value_key: name
    source: cat
    ignore_case: true

  field_producto_codigo: code

  title:
    - plugin: callback
      callable: mb_strtolower
      source: title
    - plugin: callback
      callable: ucfirst

  type:
    plugin: default_value
    default_value: producto

  field_imagen/target_id:
    plugin: entity_lookup
    entity_type: media
    bundle: image
    value_key: name
    source: image
    ignore_case: true
    access_check: false

destination:
  plugin: entity:node

migration_dependencies:
  optional:
    - migrate_files
    - media_images

Key points

  • Media entities are referenced using entity_lookup
  • Lookup is done by the media name (image filename)
  • This keeps content migrations clean and independent

Registering migrations automatically

Using config_devel, we can auto-install migrations by declaring them in the module .info.yml:

config_devel:
  install:
    - migrate_plus.migration.migrate_files
    - migrate_plus.migration.media_images
    - migrate_plus.migration.products

Running the migrations

Reload migration configuration:

drush cdi custom_migrate

Run migrations in order:

drush mim migrate_files
drush mim media_images
drush mim products

Final result

  • Files are copied and registered
  • Media entities are created and reused
  • Nodes reference Media correctly

This approach is stable, reusable and future-proof for Drupal 10 and Drupal 11 projects, and works equally well with CSV, SQL or Drupal-to-Drupal migrations.

CA

Carlos Rincón

developer

Recent Posts

Plug&Team: when technology stops being a project and becomes part of your team

Plug&Team is Omitsis’ model for integrating into enterprise teams as a true extension of the organization. Moving beyond project-based delivery, we provide continuous technological excellence, sound engineering judgment, and long-term accountability to ensure reliable execution and better strategic decisions.