Skip to content

Conversation

@jszobody
Copy link

@jszobody jszobody commented Jan 15, 2021

This PR introduces support for FilePond. It is fast becoming one of the more popular upload libraries, we use it heavily in a number of apps.

I have tried to build this with minimal impact on the existing core package, however there were a couple changes that I should walk through.

Only used for chunked uploads

If a file is smaller than the FilePond configured chunk size, it is uploaded simply in one single POST. FilePond does not include any special headers or parameters. Simple uploads from FilePond then get handled by the SingleUploadHandler, and only chunked uploads end up using this new handler.

Chunk data is raw request content

The FileReceiver assumes that if you pass in a file index, it can pull the file from the Request.

FilePond does not submit chunks as regular file uploads, rather it puts the data in the raw request payload. Someone has to pull that data, store it on disk somewhere, and create a fake UploadedFile for FileReceiver to handle.

This means my controller code was looking like this:

$path = tempnam(sys_get_temp_dir(), "upload"); file_put_contents($path, $request->getContent()); $file = new UploadedFile($path, $request->header(self::HEADER_UPLOAD_NAME), null, \UPLOAD_ERR_OK, true ); $receiver = new FileReceiver($file, $request, HandlerFactory::classFromRequest($request));

Eww. I don't like that at all. 😠

So I introduced a new factory method to FileReceiver that asks the handler class instead to prepare the file. This way a handler like FilePond can do what it knows needs to be done.

The base AbstractHandler has a new static getUploadedFile method that just mimics what FileReceiver what already doing with $fileIndexOrFile`, providing full backwards compatibility. Now any handler with weird/custom file preparation steps, like FilePond, can handle it directly.

Now my controller code is:

$receiver = FileReceiver::factory('file', $request);

Yay. 😍

The factory method gets the handler class, has the handler prepare the file, and then instantiates the FileReceiver as expected.

I'll be eager to hear your feedback on this approach. If you don't like it, one alternative would be for me to create a custom FilePondReceiver that extends FileReceiver and can handle the custom FilePond work that way. I wanted to first though try to stick with your existing core classes if possible.

File index is array

If I name a FilePond instance something like attachment and then add multiple files, it creates hidden input form fields like this:

<fieldset class="filepond--data"> <input type="hidden" name="attachment" value="9d64faec-d734-4b7c-97e8-010ed197de75"> <input type="hidden" name="attachment" value="8481bb89-95a5-44bf-b0ef-5c83efc79c1a"> <input type="hidden" name="attachment" value="573b144d-066c-4104-97ae-9b25914a728d"> <input type="hidden" name="attachment" value="c10679bb-9d90-4f73-867d-03b982e21bd9"> </fieldset>

Notice the issue? When the form is submitted, those will overwrite each other, and the server will only get one $request->input('attachment') value.

The answer is to name the FilePond instance with array syntax like attachment[]. This works great, however it means that each uploaded file (or chunk) is array wrapped as well. You'll see that the AbstractHandler::getUploadedFile method that I mentioned above also checks to see if the file is an array, and unwraps it if so. The file is always the first array element.

ENV in config file

One other change I made was to introduce a couple ENV variables in your config file. It seems to be a good practice for Laravel packages to allow ENV to control primary configs, without the need to publish the config file and edit directly. In my case for example, I will need to specify a different disk depending on my environment (local, qa, production).

The fallback values are what you had before, so this is fully backwards compatible.

Full example

My working controller code is now this:

public function upload(Request $request) { $receiver = FileReceiver::factory('file', $request); if (!$receiver->isUploaded()) { // A chunked upload is being initiated. Just send back a unique ID. return response(Str::uuid())->header('Content-Type', 'text/plain'); } $save = $receiver->receive(); if ($save->isFinished()) { // Handle the save... return $this->saveFile($save->getFile()); } // We are in chunk mode. Note that FilePond doesn't _need_ any progress // or response, this would just be for debugging client-side if needed. return response()->json([ "done" => $save->handler()->getPercentageDone(), 'status' => true ]); }

The only real differences here compared to the example repo are the 1) FileReceiver factory method, and 2) returning a unique ID (instead of throwing an exception) if no file or payload was sent.

Look forward to feedback!

@jszobody jszobody marked this pull request as ready for review January 15, 2021 18:40
@Sammyjo20
Copy link

This PR looks really good. We're using Filepond on Laravel too and we are performing chunked-uploads so this would be super handy if we could use this package! Great quality PR too.

@jszobody
Copy link
Author

jszobody commented Nov 29, 2021

@Sammyjo20 I ended up releasing a separate package here:

https://github.com/stechstudio/laravel-upload-server

If you're looking for FilePond chunking support with Laravel, you might want to take a look at that. Though I haven't updated it since it was first released, so let me know if you run into any issues.

@jszobody jszobody closed this Nov 29, 2021
@Sammyjo20
Copy link

Thanks, @jszobody - I will check it out. We actually used your PR in our project and seems to help us with a prototype for now. I will definitely keep Laravel Upload Server in mind too.

Cheers
Sam

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants