- Compatible XF Versions
- 2.3
- Visible Branding
- NO
Attachment Integrity Check
When an attachment's file goes missing but its database row survives, XenForo still renders the attachment link. Clicking it throws a 500 and fills your error log with:
There is no built-in way to find these in bulk, and no way to tell a genuinely lost attachment from one whose file is still sitting in storage under a stale key.
This happens after a database restore, a storage migration, a failed S3/R2 sync, a partially restored backup, or just years of accumulated failed deletes.
What it does
Every attachment is classified as one of three things:
Three modes
Drop the script in your forum root, next to index.php. Then:
That is read-only and changes nothing. --index does a single recursive listing of your storage and answers every lookup from memory — on remote storage that turns ~200,000 network round-trips into ~200 list calls.
Review the CSV. Then back up:
Repair without deleting anything:
Repair and purge the dead rows:
Safety
This tool can delete database rows, so it is built on the assumption that it might be wrong.
When an attachment's file goes missing but its database row survives, XenForo still renders the attachment link. Clicking it throws a 500 and fills your error log with:
Code:
League\Flysystem\FileNotFoundException: File not found at path: attachments/236/236888-0dcb....data
src/XF/Pub/View/Attachment/View.php:27
This happens after a database restore, a storage migration, a failed S3/R2 sync, a partially restored backup, or just years of accumulated failed deletes.
What it does
Every attachment is classified as one of three things:
- OK — the file is present exactly where XenForo expects it.
- RELINK — the file is missing from its expected path, but a file for this data_id exists under a different file_key and its md5 matches the stored file_hash. The upload is intact; only the database pointer is stale. Repairable with no data loss.
- MISSING — no file at all, or a candidate exists whose md5 does not match, meaning the id was reused by a later upload and that file belongs to somebody else. Nothing recoverable.
Three modes
- Scan — walks the database asking "where is the file?" and writes a CSV. Read-only.
- Orphans — the inverse. Walks storage asking "which row owns this file?", finding files stranded by a database rollback that a normal scan cannot see, because those rows no longer exist. Read-only.
- Apply — consumes the scan CSV, re-verifies every row against storage again, then repairs. Deletes only if you explicitly ask.
Drop the script in your forum root, next to index.php. Then:
Code:
php xf-missing-attachments.php --index --report=scan.csv
Review the CSV. Then back up:
Code:
mysqldump YOUR_DB xf_attachment xf_attachment_data > attach-backup.sql
Code:
php xf-missing-attachments.php --apply=scan.csv --index
Code:
php xf-missing-attachments.php --apply=scan.csv --index --delete
Safety
This tool can delete database rows, so it is built on the assumption that it might be wrong.
- Scanning never writes. Deleting takes a separate run, a separate flag, and a CSV you have had the chance to read.
- Every row is re-verified against live storage immediately before it is touched. A stale or hand-edited CSV cannot cause damage.
- Empty index aborts. If storage is unreachable or the prefix is wrong, every row would look MISSING. That is fatal and --force does not override it.
- Ratio guard. A plan that would delete more than 20% of all attachments is refused — that pattern means a storage fault, not data loss.
- Count guard. --max-delete (default 2000) blocks unexpectedly large plans.
- Deletes go through the entity layer, so attach_count is maintained by XenForo's own attachment handler for every content type, including those from other add-ons.
- Unknown arguments are rejected rather than silently ignored.
- Reports cannot be written to a web-accessible path — they list every attachment on your forum. They go to internal_data/attachment-audit/.
- Refuses to run over HTTP.