Problem/Motivation
Since 3.1.8 (#3541722) the upload form stages files under temporary://filebrowser/{nid}/ during the managed_file AJAX upload and copies them to the listing's folder when the user presses Save Upload. Those are two separate HTTP requests. On hosting where each web server has its own /tmp (Acquia Cloud Next, Pantheon, Platform.sh, any load-balanced setup without a shared temp directory) the save request often lands on a server that never received the upload, and the copy fails:
Failed to store example.pdf: File 'temporary://filebrowser/123/example.pdf' ('/tmp/filebrowser/123/example.pdf') could not be copied because it does not exist.
Two follow-on problems make this worse:
- The
$file->delete()call sits after the copy inside thetry, so the failed attempt leaves itsfile_managedrow (status 0) pointing at the temporary URI. Every retry of the same file name is then rejected by core's file URI uniqueness constraint until cron removes the row aftertemporary_maximum_age(six hours by default): The specified file example.pdf could not be uploaded. The file temporary://filebrowser/123/example.pdf already exists. Enter a unique file URI. - The form still adds "Your Filebrowser upload completed successfully." after a failed copy, and nothing is written to the log, so the failure is easy to miss.
Steps to reproduce
- Run the site on two or more web servers that do not share
/tmp, with the defaultfile_temp_path. - On a Directory Listing node, click Upload, choose a file, and press Save Upload.
- Repeat until the save request is served by a different server than the AJAX upload. The copy error above appears, the listing does not contain the file, and the success message appears as well.
- Upload the same file again: the widget itself now refuses it with the "already exists" message.
On a single server the same failure can be forced by deleting the staged file from /tmp/filebrowser/{nid}/ between the AJAX upload and pressing Save Upload.
Proposed resolution
- Stage uploads under
private://filebrowser-staging/{nid}/when the private scheme is configured, falling back totemporary://when it is not. The private file system is shared between web servers wherever the site's own files are, and it is not served to browsers. Abandoned uploads are still cleaned up by cron, because the staged file entity keeps status 0. - Delete the staged file entity whether or not the copy succeeded, so a failed save never blocks the next attempt.
- Only add the success message when no copy failed.
Remaining tasks
Review the merge request.
User interface changes
The success message is no longer shown when a file failed to store.
API changes
None. UploadForm gains a protected stagingBase() method.
Data model changes
None.
Issue fork filebrowser-3626897
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3626897-upload-staging-shared
changes, plain diff MR !17
- 3626897-uploads-fail-on
compare
Comments
Comment #2
scotwith1tComment #5
scotwith1tMR !17 against 3.1.x stages uploads under private://filebrowser-staging/{nid}/ when the private scheme is configured (falling back to temporary://), deletes the staged file entity whether or not the copy succeeded, and only reports success when nothing failed. Verified the normal upload and the missing-staged-file path on a local site.