Problem/Motivation
We currently duplicate the file field upload logic across REST, JSON API and the contrib GraphQL module.
This should perhaps be in the meta
We have 3 types of upload:
- 'Unmanged' file upload: e.g. config import tarball form upload
- File entity upload (not a field on another entity). Don't think we actually do this anywhere 🤔
- File entity upload (a field on an entity). The default
We have 4 kinds of validation:
- Symfony UploadedFile validation
- Form API 'File Upload' validation
- File field configured validation
- File Entity validation
We have two modes:
- Form upload: we can rely on the Symfony request object to get the UploadedFile objects, check for php limit errors etc
- Direct API upload: we manually parse the 'content-disposition' header and handle the upload stream ourselves
Important Note: the Symfony UploadedFile objects are created from the $_FILES superglobal. The $_FILES superglobal is only populated when the Content-Type header in the request uses application/x-www-form-urlencoded or multipart/form-data. Since REST and JSON-API only support application/octet-stream so we cannot use the Symfony UploadedFile objects for these.
See: https://www.php.net/manual/en/reserved.variables.files.php
Places in core doing file upload:
file_managed_file_save_upload(): form callback for File element.file_save_upload(): more API level called from multiple places incl. contrib. Uses UploadedFiles.\Drupal\file\Plugin\rest\resource\FileUploadResource::post()does direct file uploads via content-disposition and php:://input\Drupal\jsonapi\Controller\FileUpload::handleFileUploadForExistingResource() ::handleFileUploadForNewResourcealmost identical code to REST FileUploadResource. Does direct file uploads via content-disposition and php:://input\Drupal\ckeditor5\Controller\CKEditor5ImageController::upload()Uses UploadedFiles.
This meta issue provides the high-level overview of the tasks:
- #3221794: Unify file upload validation from rest and json api modules
- #3375447: Create an UploadedFile validator and deprecate error checking methods on UploadedFileInterface
- #3378606: Add an UploadedFiles param converter
- #3378607: Move Direct File Upload access control to the routing layer
- #2940383: [META] Unify file upload logic of REST and JSON:API
- #3375423: Deprecate file_managed_file_save_upload(), file_save_upload() and _file_save_upload_from_form() and replace with a service
- #3378607: Move Direct File Upload access control to the routing layer
- #3380345: Create an InputStreamFileWriter for writing the input stream to a file
- #3380379: Create content disposition filename extractor and deprecate duplicate code in REST and JSON API
- #3388985: Make CKEditor5ImageController reuse FileUploadHandler
Steps to reproduce
Proposed resolution
Core Managed File Upload Proposed Resolution
Here's a proposed plan for replacing:
file_managed_file_save_upload_file_save_upload_from_formfile_save_upload
With:
FileFormHelper:
- Replaces
file_managed_file_save_uploadand_file_save_upload_from_form - provides callback for
Element\ManagedFile - handles all form interactions
- getting info from form elements
- setting form errors
- setting status messages
FileFormUploader
- Replaces
file_save_upload - is passed an array of UploadedFiles rather than having to lookup from
Requestagain - handles caching of uploads
- Loops through uploads with multiple files
- handles UploadedFile::isValid checks
- Calls
FileUploadHandler::handleFileUpload()for each file - catches exceptions for each file upload
- Adds status messages for each file upload
- Returns an array of fid=>file to FileFormHelper
REST and JSON API Proposed Resolution
- Introduce a
DirectFileFieldUploadHandlerthat does most of the work - Introduce a
ContentDispositionFilenameParserfor abstracting out the work to grab the filename from the request headers - Checking access still seems complex and we need to drill down on whether there are differences between REST and JSON API. Core file upload doesn't do that at this layer. Maybe we can move it somewhere further up?
- Introduce a new
InputStreamWriterto abstract out writing php://input to a temp file. This should return an instance ofUploadedFileInterface - We still need to parse the destination directory which could contain tokens. Not sure where core file field uploads do this?
- We need to keep existing locks. Can these be moved into the
- We can re-use the existing FileUploadHandler
REST

Proposed:

JSON API file upload sequence

Proposed:

Remaining tasks
User interface changes
API changes
Data model changes
Release notes snippet
Core file_managed_file_upload sequence:

Proposed:

GraphQL upload sequence

CKEditor image upload sequence

| Comment | File | Size | Author |
|---|---|---|---|
| #33 | ckeditor-image.mermaid.txt | 875 bytes | kim.pepper |
| #33 | ckeditor-image-upload.png | 287.56 KB | kim.pepper |
Comments
Comment #2
kim.pepperComment #3
kim.pepperComment #4
kim.pepperRemoving #3221793: Move file upload validation from file.module to constraint validators from the list as I no longer feel it's a blocker for #2940383: [META] Unify file upload logic of REST and JSON:API
Comment #9
kim.pepperComment #10
kim.pepperAdded a sequence diagram to show the current state.
Mermaid definition is:
Comment #11
kim.pepperComment #12
kim.pepperHere's a sequence diagram of the REST file upload
Mermaid definition:
Comment #13
kim.pepperHere's a sequence diagram of the somewhat more complex json api file upload.
Mermaid definition:
Comment #14
kim.pepperTweaking the sequence diagrams to include more details
Comment #15
kim.pepperUpdating core file_managed_file_upload sequence
Comment #16
kim.pepperComment #17
kim.pepperAdding the graphql upload sequence.
Interestingly, GraphQL uploads raise some questions:
Content-Typeheader in the request usesapplication/x-www-form-urlencodedormultipart/form-data. Would be good to verify this?UploadedFile::move()which in turn calls PHPmove_uploaded_file()which is recommended. Again not sure why?Comment #18
kim.pepperAnother question on direct file uploads compared with Symfony
UploadedFile/$_POSTfile uploads:Are we checking for the php errors? e.g.
UPLOAD_ERR_INI_SIZE,UPLOAD_ERR_FORM_SIZE,UPLOAD_ERR_PARTIAL,UPLOAD_ERR_NO_FILE,UPLOAD_ERR_EXTENSIONetc?Looks like we are just adding a
FileSizeLimit/max_filesizevalidator. Does that cover the cases above?Comment #19
kim.pepperOther questions:
Request::getContent()does this for us?Comment #20
kim.pepperCatching up on @Wim's blog post (now unavailable) https://web.archive.org/web/20230522044139/http://wimleers.com/blog/api-...
Comment #21
kim.pepperAfter reading Wim's blog post, I think I answered a few of my own questions.
We are intentionally wanting to avoid PHP upload size limits by using php://input. Request::getContent() looks like it just just returns
fopen('php://input', 'r')for binary files anyway.Comment #22
kim.pepperHere's a proposed plan for replacing:
file_managed_file_save_upload_file_save_upload_from_formfile_save_uploadWith:
FileFormHelper:file_managed_file_save_uploadand_file_save_upload_from_formElement\ManagedFileFileFormUploaderfile_save_uploadRequestagainFileUploadHandler::handleFileUpload()for each fileComment #23
kim.pepperComment #24
kim.pepperCreated child issue #3375423: Deprecate file_managed_file_save_upload(), file_save_upload() and _file_save_upload_from_form() and replace with a service
Comment #25
kim.pepperCreated #3375447: Create an UploadedFile validator and deprecate error checking methods on UploadedFileInterface
Comment #26
kim.pepperHere's the proposed REST and JSON API sequence diagrams. Updated the proposed section in the IS.
Comment #27
andypostwould be great to unify progress tracking as well
Comment #28
matroskeenI'm not diving into the details, but I hope the contributors will keep in mind the support of resumable file uploads. Example: https://www.drupal.org/project/tus.
Comment #29
kim.pepper@Matroskeen is there anything in the proposed architecture that could be modified to make it easier to support TUS in contrib?
Comment #30
larowlanComment #31
larowlanComment #32
kim.pepperComment #33
kim.pepperAdding the sequence for
\Drupal\ckeditor5\Controller\CKEditor5ImageController::upload().As pointed out in #3372385: CKEditor file upload sets file URI prior to validation, causing validators to be unable to find the file. the order of validating the file and then moving it is the wrong way around.
Comment #34
larowlanCan we open a separate issue to try and move that to the routing layer via an access callback.
We can postpone this on that.
This happens in
\Drupal\file\Plugin\Field\FieldType\FileItem::getUploadLocation. Perhaps we can reuse that if we have a file item field (file reference) in scope?This sentence is missing a conclusion 😀
It's not stated in the issue summary, but I assume the plan for CKEditor is the same for JSONAPI/Rest? If so, can we update the issue summary there too.
I think the proposed resolutions make sense. Cast everything to an UploadedFileInterface value object seems like a good way to approach homogenizing these APIs.
I think we should explore moving the access to routing, just to confirm it can be done.
I think it would also be worth exploring a param converter at the routing level to see if we can upcast the raw POST data into UploadedFiles too, making things even more consistent.
Amazing work on the diagrams too.
Comment #35
matroskeenRe #29, Based on some work I have done in Tus issues queue (#3313657: Use file uploader service from Drupal core), I can't find anything specific. I see I added some todos, that are already planned to be addressed in Drupal core.
Speaking of other issues, I do remember that it was somehow difficult to overcome the file size validation - by default, it's looking into environment limitations, but it should not be the case when small chunks of the file are uploaded. Anyway, it seems to be out of the issue scope.
Comment #36
kim.pepperCKEditor5 is more like graphql in that it uses an UploadedFile object, rather than direct uploaded files via php://input. (Still thinking of the correct name for this!)
However, CKEditor5 should switch to FileUploadHandler.
Comment #37
kim.pepperAdded child issues:
Comment #38
kim.pepperComment #39
kim.pepperWe could do these tasks first before trying to swap out the whole file uploading for REST and JSON API at the same time:
Comment #40
kim.pepper\Drupal\ckeditor5\Controller\CKEditor5ImageControlleruses UploadedFile from the request, so it just needs to make use of FileUploadHandler. #3388985: Make CKEditor5ImageController reuse FileUploadHandlerComment #41
kim.pepperCreated #3389447: Provide a trait to create file upload validators from file field settings
Comment #42
kim.pepperTwo more issues fixed:
* #3375447: Create an UploadedFile validator and deprecate error checking methods on UploadedFileInterface
* #3380345: Create an InputStreamFileWriter for writing the input stream to a file
Comment #43
kim.pepper#3380379: Create content disposition filename extractor and deprecate duplicate code in REST and JSON API is in also.
Comment #44
wim leers#2940383: [META] Unify file upload logic of REST and JSON:API is done! 🤯
I believe that means we can close this?!
OTOH there seem to be even more file upload improvements in this meta?
Comment #45
andypostI bet there's a few issues left mostly about upload progress (apcu, vs native)
Comment #46
kim.pepperYeah we still need to do #3375423: Deprecate file_managed_file_save_upload(), file_save_upload() and _file_save_upload_from_form() and replace with a service which is mostly cleanup.
Comment #47
solideogloria commentedFrom #3282721: Files are uploaded even when forms are not submitted
I think this related issue is something that should be supported, as right now, files are uploaded prior to webform submissions when using the file upload element.