- Compatible XF 2.x 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:
Code:
League\Flysystem\FileNotFoundException: File not found at path: attachments/236/236888-0dcb....data<br>src/XF/Pub/View/Attachment/View.php:27
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:
That md5 check matters. Relinking on filename alone would staple a stranger's screenshot into somebody's post.
Three modes
Usage
Drop the script in your forum root, next to index.php. Then:
Code:
php xf-missing-attachments.php --index --report=scan.csv
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:
Code:
mysqldump YOUR_DB xf_attachment xf_attachment_data > attach-backup.sql
Repair without deleting anything:
Code:
php xf-missing-attachments.php --apply=scan.csv --index
Repair and purge the dead rows:
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.
What it never touches: posts and threads are not modified. An
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<br>src/XF/Pub/View/Attachment/View.php:27
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:
- 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.
That md5 check matters. Relinking on filename alone would staple a stranger's screenshot into somebody's post.
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.
Usage
Drop the script in your forum root, next to index.php. Then:
Code:
php xf-missing-attachments.php --index --report=scan.csv
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:
Code:
mysqldump YOUR_DB xf_attachment xf_attachment_data > attach-backup.sql
Repair without deleting anything:
Code:
php xf-missing-attachments.php --apply=scan.csv --index
Repair and purge the dead rows:
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.
What it never touches: posts and threads are not modified. An