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 the try, so the failed attempt leaves its file_managed row (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 after temporary_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

  1. Run the site on two or more web servers that do not share /tmp, with the default file_temp_path.
  2. On a Directory Listing node, click Upload, choose a file, and press Save Upload.
  3. 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.
  4. 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 to temporary:// 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.

Command icon 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:

Comments

scotwith1t created an issue. See original summary.

scotwith1t’s picture

Issue summary: View changes

scotwith1t changed the visibility of the branch 3626897-uploads-fail-on to hidden.

scotwith1t’s picture

Status: Active » Needs review

MR !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.