-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Description
Environment
- Nextcloud 31.0.7.2 (Enterprise)
- PHP 8.3.25 (FPM) / Linux 5.14
- MariaDB 10.11.10
- Apps:
files_trashbin1.21.0,files_versionsenabled - External storage: Amazon S3 via files_external
- Home storage: local Lustre (
/mnt/lustre01/nextcloud/data/<user_placeholder>)
Steps to reproduce
- Mount an S3 bucket and upload a large file that has no
oc_filecacherow. - Delete it via WebDAV/UI (triggering
Trashbin::move2trash()). - Terminate the PHP worker (or inject a fatal) after
$trashStorage->moveFromStorage()but before$query->insert('files_trash').
Actual result
/data/<user_placeholder>/files_trashbin/files/<file_placeholder>.d<ts>exists on disk and consumes quota.oc_files_trashlacks the<file_placeholder>, <ts>row; trash UI cannot show the item.oc_filecachelacks the entry; onlyocc files:scan --path="/<user_placeholder>/files_trashbin/files/<file_placeholder>.d<ts>"makes it visible, without location metadata.
Expected result
The trash move should be atomic so payload, metadata, and cache entry all succeed or the operation rolls back with no orphaned blob.
Initial findings
- In
Trashbin::move2trash()(apps/files_trashbin/lib/Trashbin.php) the storage copy runs before theINSERTintooc_files_trashwith no transaction binding the two. A crash between them leaves the file behind. - Cache updates are skipped whenever the source file was not already in cache (
$inCache === false), so the trash copy never gets anoc_filecacherow. - Permanent deletion (
Trashbin::delete()) and the cron expire job delete DB rows before removing files, so the same race can occur in reverse.
Logs / evidence
{"reqId":"","time":"2025-10-30 08:56:29","user":"user12345","app":"admin_audit","method":"DELETE","url":"/remote.php/dav/files/user12345/external_mount/bigfile01","message":"File with id "4553095" deleted","version":"31.0.7.2"}
MariaDB> SELECT * FROM oc_files_trash WHERE timestamp='1761782189';
+---------+-----------+------------+------------+----------------+------+-------+------------+
| auto_id | id | user | timestamp | location | type | mime | deleted_by |
+---------+-----------+------------+------------+----------------+------+-------+------------+
| 393039 | bigfile02 | user12345 | 1761782189 | external_mount | NULL | NULL | user12345 |
+---------+-----------+------------+------------+----------------+------+-------+------------+
MariaDB> SELECT * FROM oc_filecache WHERE path LIKE 'files_trashbin/files/bigfile01%';
-- no rows --
$ ls -lh /mnt/lustre01/nextcloud/data/user12345/files_trashbin/files
-rw-r--r-- 1 nginx nginx 10G Oct 30 08:56 bigfile01.d1761782189
-rw-r--r-- 1 nginx nginx 10G Oct 30 08:56 bigfile02.d1761782189
Proposed fix
- Wrap the payload copy and DB insert in a transaction (or reverse order) and delete the copied file or rollback cache if the insert fails.
- Always populate the
oc_filecacheentry for the trash copy, even when the source was not cached. - Apply transactional ordering to
Trashbin::delete()and the cron expiration path. - Improve logging when the insert fails so admins can diagnose issues without digging through PHP-FPM logs.