Skip to content

Trashbin copy leaves blob in /files_trashbin without oc_files_trash / filecache rows when request aborts after moveFromStorage #56493

@hweihwang

Description

@hweihwang

Environment

  • Nextcloud 31.0.7.2 (Enterprise)
  • PHP 8.3.25 (FPM) / Linux 5.14
  • MariaDB 10.11.10
  • Apps: files_trashbin 1.21.0, files_versions enabled
  • External storage: Amazon S3 via files_external
  • Home storage: local Lustre (/mnt/lustre01/nextcloud/data/<user_placeholder>)

Steps to reproduce

  1. Mount an S3 bucket and upload a large file that has no oc_filecache row.
  2. Delete it via WebDAV/UI (triggering Trashbin::move2trash()).
  3. 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_trash lacks the <file_placeholder>, <ts> row; trash UI cannot show the item.
  • oc_filecache lacks the entry; only occ 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 the INSERT into oc_files_trash with 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 an oc_filecache row.
  • 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_filecache entry 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.

Metadata

Metadata

Assignees

Labels

0. Needs triagePending check for reproducibility or if it fits our roadmap31-feedbackbug

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions