A CLI migration utility for ExpressionEngine that converts legacy EEHarbor Assets fields into native ExpressionEngine File fields — in place.
- Field IDs, short names (handles), labels, and channel/site assignments are preserved. Your templates keep working under the same field names.
- Entry values are rewritten as modern
{file:ID:url}references. - Native file records are created (via EE's own sync machinery) for files EE does not know yet; Assets per-file metadata (title, description / alt text, author, location) is copied onto native records where the native side is empty.
- Assets data (
exp_assets_*) is never modified — it remains your rollback source of truth until you uninstall Assets.
MIT licensed. Community utility, not a commercial migration framework.
This tool modifies field definitions and entry data. Before any live run:
- Create a database backup — e.g.
php system/ee/eecli.php backup:database. - Test the full migration on a staging copy before production.
There is no rollback command. Recovery is: restore the backup.
- ExpressionEngine 7.4.0 or later
- Assets 5.0.7 or later (its tables must still exist)
file_manager_compatibility_modeoff (the modern file manager) — the command aborts otherwise- PHP CLI access to run
eecli.php
Copy the assets_to_files folder into system/user/addons/. That's it —
CLI commands are discovered without installing the add-on in the control
panel. (If your EE version does not list the command, run
php system/ee/eecli.php addons:install assets_to_files.)
php system/ee/eecli.php assets-to-files:migrate [options]
| Option | Effect |
|---|---|
--dry-run, -d |
Validate and report only. Nothing is written. |
--audit |
Adds filesystem-heavy checks: per-file existence/readability, rename predictions, clean-name collisions. |
--field=12,18 / -f |
Migrate only these fields (numeric IDs or short names). Default: every Assets field. |
--multi-file=strict|skip |
Entries holding multiple assets: strict (default) marks the whole field unsupported; skip migrates the field and leaves those entries untouched, listed for manual cleanup. |
--yes, -y |
Skip the backup confirmation prompt. |
# 1. See what would happen, including filesystem verification
php system/ee/eecli.php assets-to-files:migrate --dry-run --audit
# 2. Migrate the smallest field first and spot-check it in the CP
php system/ee/eecli.php assets-to-files:migrate --field=26
# 3. Re-run the same field: everything should report "already migrated"
php system/ee/eecli.php assets-to-files:migrate --field=26 --dry-run
# 4. Migrate the rest
php system/ee/eecli.php assets-to-files:migrateFiles that have no native exp_files record yet are registered one by
one through EE's sync machinery (which also generates thumbnails). If the
dry run reports many files "would be registered", it is faster to bulk
register them first with core's own tooling, then migrate:
php system/ee/eecli.php sync:upload-directory --upload-id=1The migration only touches files actually referenced by content — library
size in bytes is irrelevant; only the number of referenced files matters.
For very large sites, run field by field (--field=...); the command is
safe to re-run at any time and resumes where it left off.
Entry values are written through ChannelEntry models to keep EE's file
usage bookkeeping correct, which means every normal per-save side effect
fires once per migrated entry. Three of those dominate the runtime on a
large field, so the command switches them off for the duration of the write
batch and restores them afterwards:
- Entry revisions. Writing a revision per save is the most expensive
part of the save, and it evicts real editor revisions (
max_revisionsis usually 3, and an entry in several migrated fields is saved once per field). Suppressed — see the revisions caveat below. - Cache-breaking extensions, e.g. BoldMinded Speedy.
- Search indexers, e.g. Pro Search, which deletes and fully re-tokenises each entry on every save.
Extension suspension is purely in-memory — exp_extensions is never
touched, so nothing is disabled outside the running process. Whatever was
suspended is listed under Follow-up at the end of the run, together
with the one-off command or CP action that makes up for it.
The write phase reports its own throughput (wrote N entries in X.Xs), so
you can measure the effect of any of this on your own data rather than
taking the above on trust.
- Reads
exp_assets_selections(the authoritative entry↔file link), joins Assets file and folder records, and resolves every selection to an upload directory + relative path. - Prunes: selections of deleted entries, duplicate selections of the
same asset, entries already holding
{file:ID:url}values, and multi-file entries per--multi-file. - Resolves each path against native
exp_filesrecords (including subfolder trees). Paths claimed by more than one native record are reported as collisions and skipped rather than guessed at. - Registers missing native records via EE's
syncFiles()and copies Assets metadata onto native records (only into empty fields). - Converts the field definition to
filewith clean native settings (allowed directory translated from Assetsfiledirs; Assets UI preferences discarded). - Rewrites entry values in batches through ChannelEntry models, so
exp_file_usage, file usage counts, and caches stay correct.
Re-running is always safe:
- Fields already converted report "already migrated".
- Entries already holding a
{file:ID:url}value are skipped. - A killed run (timeout, ctrl-C) resumes by simply running the command again. There are no state files or tracker tables.
- File renames: when registering a file whose name fails EE's
cleaning rules (illegal characters etc.), EE physically renames the
file on disk. The report lists every rename (
FILE_RENAMED_ON_SYNC). Run--dry-run --auditfirst to see predicted renames. - Entry revisions: migrated entries do not gain a revision, even on
channels with versioning enabled. This is deliberate — the revision would
be written after the change (so it is useless for rollback) while pushing
a real editor revision out of
max_revisions. Existing history is left intact.versioning_enabledis restored per batch; if the process is hard-killed (SIGKILL, power loss) mid-batch, up to one batch of entries can be left with versioning off. Re-running the migration will not repair it — those entries are already migrated and get skipped — so fix it directly:UPDATE exp_channel_titles SET versioning_enabled = 'y' WHERE entry_id IN (...); - Settings widening: Assets fields allowing multiple upload directories become native fields allowing all directories (native File fields support one directory or all).
- Multi-site (MSM): newly registered files are stamped with the
current site context's
site_id. On MSM installs with cross-site upload directories, verify the results on staging first.
Not supported, by design: rollback, Grid/File Grid/Matrix content, multi-file values (beyond skip-and-report), S3/remote Assets sources, Pro Variables content, EE Pro drafts, CP interface, template rewriting. Fields or entries in these situations are reported and left untouched.
Keep the Assets add-on installed until you have verified the site.
Uninstalling Assets drops all exp_assets_* tables — that is the
point of no return. Once verified:
- Update any Assets-specific template syntax (this tool does not touch
templates; preserved field handles keep
{field_name}tags working for common cases, but Assets tag pairs/modifiers need review). - Uninstall Assets.