Skip to main content

When working with Drupal environments, there are times you need to duplicate a directory structure — for example, cloning a site or preparing a new environment without copying certain environment-specific folders.

A common case is excluding:

sites/default/files

This directory typically contains:

  • User uploads
  • Generated image styles
  • Cached assets
  • Environment-specific permissions

Copying it between environments can cause permission issues, unnecessary bloat, or unintended data leakage.

This article walks through the **correct and safe way** to copy a directory **while excluding a specific child directory**.

The problem with `cp -R`

A naive approach might look like this:

sudo cp -R ../frdc/sites frdc-12-01-2026/

While this works, it has a critical limitation:

> `cp` does not support exclusions.

There is no reliable way to tell `cp`:

> Copy everything *except* `sites/default/files`

Any workaround (using `find`, temporary deletes, or tar pipelines) introduces unnecessary risk — especially on a live Drupal codebase.

The correct tool: `rsync`

`rsync` is purpose-built for controlled file transfers and directory synchronisation.
It supports exclusions, preserves permissions, and is installed by default on most Linux and macOS systems.

Recommended command

rsync -av \
 --exclude='default/files' \
 ../frdc/sites/ \
 frdc-12-01-2026/sites/

What this command does

Let’s break it down:

  • rsync - robust file copy and sync tool
  • -a - archive mode (preserves permissions, ownership, symlinks, timestamps)
  • -v - verbose output (see what’s happening)
  • --exclude='default/files' - excludes only that directory
  • ../frdc/sites/ - source directory
  • frdc-12-01-2026/sites/ - destination directory

The trailing slash matters

Including `/sites/` (not `/sites`) means:

  • Copy the *contents* of `sites`
  • Not nest `sites/sites` accidentally

Resulting directory structure

After running the command, the destination will look like this:

sites/
└── default/
   ├── settings.php
   ├── services.yml
   ├── default.services.yml
   ├── settings.local.php
   └── files/        ❌ not copied

Everything under `sites/default` is copied except `files`.

Always do a dry run first

Before executing the real copy, it’s good practice to validate what will happen:

rsync -av --exclude='default/files' --dry-run \
 ../frdc/sites/ \
 frdc-12-01-2026/sites/

This shows exactly:

  • What files will be copied
  • What will be skipped
  • Without touching the filesystem

Why this approach is best practice for Drupal

Excluding `sites/default/files` is intentional and recommended because:

  • Files are environment-specific
  • Permissions often differ across DEV / UAT / PROD
  • Files can be synced separately if required
  • Drupal can regenerate styles and caches
  • Avoids copying large, unnecessary data

In most deployment workflows:

  • Code moves forward
  • Files are either shared, mounted, or synced independently

When to extend this approach

You can easily expand the exclusions if needed:

--exclude='default/files' \
--exclude='private' \
--exclude='translations'

Or externalise them into an exclude file for reuse in pipelines.

 

If you need to copy a directory but exclude a specific child directory, the rule is simple:

> Don’t use `cp`. Use `rsync`.

It’s safer, clearer, and designed for exactly this scenario - especially in Drupal and multi-environment deployments.