db-import / db-export : deux commandes DDEV globales pour gérer ses dumps

À force de jongler entre plusieurs projets DDEV, j'ai fini par me lasser de réécrire les mêmes petits scripts d'import et d'export de base dans chaque dépôt. Heureusement : DDEV sait charger des commandes personnalisées globales. Déposées dans ~/.ddev/commands/host/, elles sont disponibles dans tous les projets DDEV de la machine, sans rien installer côté projet. Voici les deux que j'utilise au quotidien : ddev db-import et ddev db-export.

Sera pris comme paradigme que pour chaque projet mes dumps sont dans un dossier ./files/dumps depuis la racine du projet

ddev db-import : restaurer une base

La commande vide la base, importe un dump, puis enchaîne drush deploy, drush cr et drush uli — de quoi repartir d'un dump de prod fraîchement récupéré en une seule commande. Sans argument, elle prend le dump le plus récent (par date de modification) du dossier de dumps :

ddev db-import                         # dump le plus récent
ddev db-import dump.sql.gz             # nom seul : cherché dans le dossier de dumps
ddev db-import chemin/vers/dump.sql.gz # dump précis
ddev db-import -l                      # liste les dumps, du plus récent au plus ancien

L'argument est résolu d'abord tel quel (chemin absolu ou relatif à la racine du projet), puis dans le dossier de dumps — un simple nom de fichier suffit donc. Les formats gérés sont ceux de ddev import-db : .sql, .sql.gz, .sql.bz2, .sql.xz, .mysql, .mysql.gz, .zip, .tgz, .tar.gz.

Point important : la base est bien vidée avant l'import. C'est ddev import-db qui s'en charge (il droppe la base par défaut, cf. son flag --no-drop) — plus robuste qu'un drush sql:drop, qui exige un Drupal capable de bootstrapper… ce qui n'est pas gagné quand la base est justement dans les choux.

Dernier confort en date : le nom du dump s'autocomplète. ddev db-import <tab> propose les dumps du dossier de dumps, du plus récent au plus ancien — le mécanisme est détaillé plus bas.

ddev db-export : dumper la base

Le pendant de l'import : la commande vide les caches (drush cr, pour ne pas embarquer des tables de cache pleines) puis exporte vers <dossier>/<horodatage>-<projet>-dev.sql.gz. Compressé par défaut :

ddev db-export             # -> 2026-07-27_14-30-00-monprojet-dev.sql.gz
ddev db-export --no-gzip   # -> .sql non compressé
ddev db-export -n          # dry-run
ddev db-export --no-cr     # sans vidage de cache préalable

Le nom de fichier horodaté fait que le « dump le plus récent » de db-import tombe toujours juste : les deux commandes se complètent naturellement.

Le dossier de dumps : DB_DUMP_DIR

Les deux commandes partagent le même dossier de dumps : files/dumps par défaut (relatif à la racine du projet), surchargeable par projet via la variable DB_DUMP_DIR, dans le .env à la racine ou dans .ddev/.env, en chemin relatif ou absolu :

# .env du projet — la valeur de base (celle utilisée sans configuration)
DB_DUMP_DIR=files/dumps

# surchargeable par un autre dossier, relatif à la racine du projet…
DB_DUMP_DIR=dumps
# … ou absolu
DB_DUMP_DIR=/chemin/absolu/vers/dumps

La priorité est : variable d'environnement > .env > .ddev/.env > défaut. db-export crée le dossier s'il manque ; db-import échoue proprement s'il n'existe pas. Détail qui a son importance : le .env n'est jamais sourcé — la variable est extraite par grep, car sourcer un .env revient à exécuter son contenu.

Le code

Les fichiers sont à déposer dans ~/.ddev/commands/host/ (et à rendre exécutables : chmod +x). Pas besoin de redémarrer quoi que ce soit, DDEV les découvre tout seul.

~/.ddev/commands/host/db-import

#!/usr/bin/env bash

## Description: Vide la base, importe le dump le plus récent (files/dumps par défaut), puis drush deploy + cr + uli
## Usage: db-import [-y] [-n] [-l] [fichier]
## Example: "ddev db-import" or "ddev db-import dev_2026-04-14.sql.gz" or "ddev db-import files/dumps/dev_2026-04-14.sql.gz" or "ddev db-import -l" or "ddev db-import -n" or "ddev db-import -y"
## Flags: [{"Name":"yes","Shorthand":"y","Type":"bool","Usage":"Ne pas demander de confirmation"},{"Name":"dry-run","Shorthand":"n","Type":"bool","Usage":"Affiche ce qui serait fait, sans rien exécuter"},{"Name":"list","Shorthand":"l","Type":"bool","Usage":"Liste les dumps disponibles, du plus récent au plus ancien"},{"Name":"file","Shorthand":"f","Type":"string","Usage":"Nom ou chemin d'un dump précis à importer"}]
##
## Le fichier passé en argument est cherché tel quel (chemin absolu ou relatif
## à la racine du projet), puis dans le dossier de dumps : un simple nom de
## fichier suffit donc.
##
## Le dossier des dumps est "files/dumps" par défaut. Surchargeable par projet
## via la variable DB_DUMP_DIR dans le .env du projet (ou .ddev/.env) :
##   DB_DUMP_DIR=dumps
##   DB_DUMP_DIR=/chemin/absolu/vers/dumps

set -euo pipefail

DEFAULT_DUMP_DIR="files/dumps"
ENV_VAR_NAME="DB_DUMP_DIR"
# Extensions gérées par "ddev import-db".
EXTENSIONS=(sql sql.gz sql.bz2 sql.xz mysql mysql.gz zip tgz tar.gz)

APPROOT="${DDEV_APPROOT:-$PWD}"
cd "$APPROOT"

assume_yes=false
dry_run=false
list_only=false
dump_file=""

while [ $# -gt 0 ]; do
  case "$1" in
    -y|--yes)     assume_yes=true ;;
    -n|--dry-run) dry_run=true ;;
    -l|--list)    list_only=true ;;
    -f|--file)    shift; dump_file="${1:-}" ;;
    --file=*)     dump_file="${1#--file=}" ;;
    -*)           echo "Option inconnue : $1" >&2; exit 1 ;;
    *)            dump_file="$1" ;;
  esac
  shift
done

# Lit une variable dans un fichier .env sans le sourcer (pas d'exécution de code).
read_env_var() {
  local file="$1" key="$2" line value
  [ -f "$file" ] || return 1
  line="$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=" "$file" 2>/dev/null | tail -n 1 || true)"
  [ -n "$line" ] || return 1

  value="${line#*=}"
  value="${value#"${value%%[![:space:]]*}"}"   # trim gauche

  if [ "${value:0:1}" = '"' ]; then
    value="${value#\"}"; value="${value%%\"*}"
  elif [ "${value:0:1}" = "'" ]; then
    value="${value#\'}"; value="${value%%\'*}"
  else
    value="${value%%#*}"                        # commentaire de fin de ligne
    value="${value%"${value##*[![:space:]]}"}"  # trim droite
  fi

  [ -n "$value" ] || return 1
  printf '%s' "$value"
}

# Priorité : environnement > .env (racine) > .ddev/.env > défaut.
dump_dir="${!ENV_VAR_NAME:-}"
dump_dir_origin="variable d'environnement"
if [ -z "$dump_dir" ]; then
  if dump_dir="$(read_env_var "$APPROOT/.env" "$ENV_VAR_NAME")"; then
    dump_dir_origin=".env"
  elif dump_dir="$(read_env_var "$APPROOT/.ddev/.env" "$ENV_VAR_NAME")"; then
    dump_dir_origin=".ddev/.env"
  else
    dump_dir="$DEFAULT_DUMP_DIR"
    dump_dir_origin="défaut"
  fi
fi

case "$dump_dir" in
  "~/"*) dump_dir="$HOME/${dump_dir#\~/}" ;;
esac
case "$dump_dir" in
  /*) ;;
  *) dump_dir="$APPROOT/$dump_dir" ;;
esac

# Un dump explicite court-circuite la recherche : d'abord tel quel (chemin
# absolu ou relatif au projet), sinon relativement au dossier de dumps, ce qui
# permet de ne passer que le nom du fichier.
if [ -n "$dump_file" ]; then
  case "$dump_file" in
    /*) ;;
    *)
      if [ ! -f "$dump_file" ] && [ -f "$dump_dir/$dump_file" ]; then
        dump_file="$dump_dir/$dump_file"
      fi
      ;;
  esac
  [ -f "$dump_file" ] || {
    echo "Fichier introuvable : $dump_file" >&2
    echo "Cherché depuis $APPROOT, puis dans $dump_dir ($dump_dir_origin)." >&2
    echo "\"ddev db-import -l\" liste les dumps disponibles." >&2
    exit 1
  }
else
  [ -d "$dump_dir" ] || {
    echo "Dossier de dumps introuvable : $dump_dir ($dump_dir_origin)" >&2
    echo "Définis $ENV_VAR_NAME dans le .env du projet pour le surcharger." >&2
    exit 1
  }

  find_args=()
  for ext in "${EXTENSIONS[@]}"; do
    [ ${#find_args[@]} -gt 0 ] && find_args+=(-o)
    find_args+=(-name "*.${ext}")
  done

  # mtime<TAB>chemin, trié du plus récent au plus ancien.
  mapfile -t dumps < <(find "$dump_dir" -maxdepth 1 -type f \( "${find_args[@]}" \) -printf '%T@\t%p\n' | sort -rn)

  [ ${#dumps[@]} -gt 0 ] || { echo "Aucun dump trouvé dans $dump_dir" >&2; exit 1; }

  if $list_only; then
    echo "Dumps disponibles dans $dump_dir ($dump_dir_origin), du plus récent au plus ancien :"
    for entry in "${dumps[@]}"; do
      path="${entry#*$'\t'}"
      printf '  %s  %s  %s\n' \
        "$(date -d "@${entry%%$'\t'*}" '+%Y-%m-%d %H:%M')" \
        "$(du -h "$path" | cut -f1)" \
        "$(basename "$path")"
    done
    exit 0
  fi

  dump_file="${dumps[0]#*$'\t'}"
fi

# Le dump peut venir d'ailleurs que du dossier de dumps (chemin explicite).
dump_parent="$(cd "$(dirname "$dump_file")" && pwd)"
if [ -d "$dump_dir" ] && [ "$dump_parent" = "$(cd "$dump_dir" && pwd)" ]; then
  dump_dir_display="$dump_dir ($dump_dir_origin)"
else
  dump_dir_display="$dump_parent"
fi

echo "Projet     : ${DDEV_PROJECT:-?}"
echo "Dossier    : $dump_dir_display"
echo "Dump       : $(basename "$dump_file") ($(du -h "$dump_file" | cut -f1), $(date -r "$dump_file" '+%Y-%m-%d %H:%M'))"
echo "Actions    : vidage base + import, puis drush deploy, drush cr, drush uli"
echo

if $dry_run; then
  echo "[dry-run] Commandes qui seraient exécutées :"
  echo "  ddev import-db --file=$dump_file"
  echo "  ddev drush deploy"
  echo "  ddev drush cr"
  echo "  ddev drush uli"
  exit 0
fi

if ! $assume_yes && [ "${DDEV_NONINTERACTIVE:-}" != "true" ]; then
  read -rp "La base de ${DDEV_PROJECT:-ce projet} va être ÉCRASÉE. Continuer ? [y/N] " answer
  case "$answer" in
    [Yy]|[Yy][Ee][Ss]) ;;
    *) echo "Annulé."; exit 0 ;;
  esac
fi

# import-db vide la base avant d'importer (comportement par défaut, cf. --no-drop).
echo "==> Vidage de la base et import du dump"
ddev import-db --file="$dump_file"

echo "==> drush deploy"
ddev drush deploy

echo "==> drush cr"
ddev drush cr

echo "==> drush uli"
ddev drush uli

~/.ddev/commands/host/db-export

#!/usr/bin/env bash

## Description: Vide les caches puis exporte la base dans files/dumps/<date>-<projet>-dev.sql.gz
## Usage: db-export [--no-gzip] [-n] [--no-cr]
## Example: "ddev db-export" or "ddev db-export --no-gzip" or "ddev db-export -n" or "ddev db-export --no-cr"
## Flags: [{"Name":"no-gzip","Type":"bool","Usage":"Exporte en .sql non compressé"},{"Name":"dry-run","Shorthand":"n","Type":"bool","Usage":"Affiche ce qui serait fait, sans rien exécuter"},{"Name":"no-cr","Type":"bool","Usage":"Ne pas vider les caches avant l'export"}]
##
## Pendant de "ddev db-import". Le dossier de destination est "files/dumps" par
## défaut, surchargeable par projet via DB_DUMP_DIR dans le .env (ou .ddev/.env) :
##   DB_DUMP_DIR=dumps
##   DB_DUMP_DIR=/chemin/absolu/vers/dumps

set -euo pipefail

DEFAULT_DUMP_DIR="files/dumps"
ENV_VAR_NAME="DB_DUMP_DIR"
SUFFIX="dev"

APPROOT="${DDEV_APPROOT:-$PWD}"
cd "$APPROOT"

gzip_it=true
dry_run=false
do_cr=true

while [ $# -gt 0 ]; do
  case "$1" in
    --no-gzip)    gzip_it=false ;;
    -n|--dry-run) dry_run=true ;;
    --no-cr)      do_cr=false ;;
    -*)           echo "Option inconnue : $1" >&2; exit 1 ;;
    *)            echo "Argument inattendu : $1" >&2; exit 1 ;;
  esac
  shift
done

# Lit une variable dans un fichier .env sans le sourcer (pas d'exécution de code).
# Dupliqué depuis db-import pour garder chaque commande autonome.
read_env_var() {
  local file="$1" key="$2" line value
  [ -f "$file" ] || return 1
  line="$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=" "$file" 2>/dev/null | tail -n 1 || true)"
  [ -n "$line" ] || return 1

  value="${line#*=}"
  value="${value#"${value%%[![:space:]]*}"}"   # trim gauche

  if [ "${value:0:1}" = '"' ]; then
    value="${value#\"}"; value="${value%%\"*}"
  elif [ "${value:0:1}" = "'" ]; then
    value="${value#\'}"; value="${value%%\'*}"
  else
    value="${value%%#*}"                        # commentaire de fin de ligne
    value="${value%"${value##*[![:space:]]}"}"  # trim droite
  fi

  [ -n "$value" ] || return 1
  printf '%s' "$value"
}

# Priorité : environnement > .env (racine) > .ddev/.env > défaut.
dump_dir="${!ENV_VAR_NAME:-}"
dump_dir_origin="variable d'environnement"
if [ -z "$dump_dir" ]; then
  if dump_dir="$(read_env_var "$APPROOT/.env" "$ENV_VAR_NAME")"; then
    dump_dir_origin=".env"
  elif dump_dir="$(read_env_var "$APPROOT/.ddev/.env" "$ENV_VAR_NAME")"; then
    dump_dir_origin=".ddev/.env"
  else
    dump_dir="$DEFAULT_DUMP_DIR"
    dump_dir_origin="défaut"
  fi
fi

case "$dump_dir" in
  "~/"*) dump_dir="$HOME/${dump_dir#\~/}" ;;
esac
case "$dump_dir" in
  /*) ;;
  *) dump_dir="$APPROOT/$dump_dir" ;;
esac

sitename="${DDEV_SITENAME:-${DDEV_PROJECT:-db}}"
filename="$(date +%Y-%m-%d_%H-%M-%S)-${sitename}-${SUFFIX}.sql"
$gzip_it && filename="${filename}.gz"
target="$dump_dir/$filename"

echo "Projet     : ${DDEV_PROJECT:-?}"
echo "Dossier    : $dump_dir ($dump_dir_origin)"
echo "Fichier    : $filename"
echo "Actions    : $($do_cr && echo "drush cr, puis " || true)export de la base"
echo

if $dry_run; then
  echo "[dry-run] Commandes qui seraient exécutées :"
  $do_cr && echo "  ddev drush cr"
  echo "  ddev export-db --gzip=$($gzip_it && echo true || echo false) --file=$target"
  exit 0
fi

mkdir -p "$dump_dir"

if $do_cr; then
  echo "==> drush cr"
  ddev drush cr
fi

echo "==> Export de la base"
if $gzip_it; then
  ddev export-db --gzip=true --file="$target"
else
  ddev export-db --gzip=false --file="$target"
fi

echo
echo "Dump créé : $target ($(du -h "$target" | cut -f1))"

 

Écrit par Kevin Gautreau — développeur & formateur PHP/Drupal freelance en Auvergne. me contacter

Contenus en rapport

snippet · 30 Mar 2022 Installer DDEV sur ubuntu DDEV est une couche d'abstraction à docker et docker-compose permettant de mettre en place une infrastructure pour développer simplement sur un projet drupal…

Commentaires (0)