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/filesThis 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 directoryfrdc-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 copiedEverything 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.