This script moves files from the Immich upload directory to a defined external library folder and updates their corresponding entries in the PostgreSQL database.
- Moves non-hidden files from the upload directory into a permanent external library
- Updates PostgreSQL records to reflect the new file paths and marks assets as external library items
- Sets
isExternal,deviceId, andlibraryIdfields to prevent Immich from re-importing files - Prevents duplicate moves (
mv -n) - Fully configurable via environment variables at the top of the script
- Preserves original asset's file name
--dry-runmode shows exactly what would change before any move/UPDATE- File-level locking prevents concurrent runs from corrupting state
- NOT EXISTS guard against the
(ownerId, libraryId, checksum)UNIQUE constraint — content-duplicates already in the external library are skipped cleanly instead of aborting and leaving a half-migrated state - Restores the moved file to source if the DB UPDATE skips it (no leaked files)
- Per-file progress + final summary counters
- Bash shell
psql(PostgreSQL client) — version 14+ recommended- Access to Immich's PostgreSQL database
All configuration is via environment variables (or by editing the defaults at the top of the script):
SRC_DIR=/opt/immich/upload/upload/ # default for Proxmox-helper-scripts installs
DEST_DIR=/mnt/external_library/
LIBRARY_NAME="External Library" # name of your external library in Immich
PGDATABASE=immich
PGUSER=immich
PGHOST=localhost
PGPORT=5432
PGPASSWORD=password
LOCK_FILE=/tmp/immich-migrate-to-external-library.lock
For better security, use a ~/.pgpass file instead of PGPASSWORD.
chmod +x script.sh
./script.sh --dry-run --verbose # preview what would happen
./script.sh # apply changes
./script.sh --verbose # apply + log every file (not just summary)
./script.sh --help # show usageThe script will:
- Find all non-hidden files under
SRC_DIR - For each file, look up the asset record by
originalPath - Move the file to
DEST_DIR/<originalFileName>(preserving the asset's name) - UPDATE the asset's
originalPath,isExternal,deviceId,libraryIdin one atomic CTE - If the UPDATE is skipped by the NOT EXISTS guard (content duplicate), restore the file to
SRC_DIRso the source is unchanged
- Create your external library in Immich first and set
LIBRARY_NAMEto match its name exactly. - Duplicate filenames across different folders in
SRC_DIRwill collide atDEST_DIR/<originalFileName>. The script's[[ -e ... ]]check +mv -nprevents overwrite — these files will be skipped (counterSKIPPED_DEST_EXISTS). Re-run after resolving manually. - Don't rewrite
checksumto sha1-path (the format Immich's library scan creates). The mobile bulk-upload-check matches by content sha1 only — switching to sha1-path will trigger the iOS/Android client to re-upload its entire library. KeepchecksumAlgorithm = 'sha1'(which this script does, by leaving the field unchanged). - Use version control / database backups before running on production data.
pg_dump immichis your friend. - The script preserves the asset's
originalFileNameas the destination — if you want subdirectory organization (e.g., year/month folders), edit thenew_pathline in the script.
- It does not migrate files in trashed assets (
deletedAt IS NOT NULL). - It does not migrate already-external assets (
libraryId IS NOT NULL). - It does not change checksums (correctly — see caveat above).
- It does not handle the case of a duplicate already in the external library (gracefully skips with the NOT EXISTS guard).