Chunked File Upload with Laravel and Vue

Posted on Updated on LaravelVue.js by Gergő D. Nagy

Uploading files can be really pained in the neck, especially if we don’t really want to overcontrol the user. Using chunking and uploading them one by one can be a good alternative that avoids errors like reaching max post size.

The Concept

The concept is quite simple and it’s applicable for multiple files as well, not only one. We’ll take the single upload version to keep this simple, but it gives a good indication of how to handle multiple files as well. So, let’s take a look at the steps:

  • The user selects the file,
  • In the Vue component, we slice the file using the predefined filesize,
  • We push the chunks to a queue – which is a simple array,
  • We upload the chunks one by one, after each other,
  • On the back-end, we store the file temporarily and append the content of the chunks,
  • When the last chunk is uploaded successfully, we rename the file on the back-end and move it to its final location.

Sounds good, so far. Now let’s take a look at the code.

The Uploader Component

Let’s see the code first, then we will explain the things:

<script>
    export default {
        watch: {
            queue(n, o) {
                if (n.length > 0) {
                    this.upload();
                }
            }
        },

        data() {
            return {
                file: null,
                chunks: [],
                error: null,
                uploaded: 0
            };
        },

        computed: {
            progress() {
                return Math.floor((this.uploaded * 100) / this.file.size);
            },
            formData() {
                let formData = new FormData;

                formData.set('is_last', this.chunks.length === 1);
                formData.set('file', this.chunks[0], `${this.file.name}.part`);

                return formData;
            },
            config() {
                return {
                    method: 'POST',
                    data: this.formData,
                    url: 'api/upload',
                    headers: {
                        'Content-Type': 'application/octet-stream'
                    },
                    onUploadProgress: event => {
                        this.uploaded += event.loaded;
                    }
                };
            }
        },

        methods: {
            select(event) {
                this.file = event.target.files.item(0);
                this.createChunks();
            },
            upload() {
                axios(this.config).then(response => {
                    this.chunks.shift();
                }).catch(error => {});
            },
            createChunks() {
                let size = 2048, chunks = Math.ceil(this.file.size / size);

                for (let i = 0; i < chunks; i++) {
                    this.chunks.push(this.file.slice(
                        i * size, Math.min(i * size + size, this.file.size), this.file.type
                    ));
                }
            }
        }
    }
</script>

<template>
    <div>
        <input type="file" @change="select">
        <progress :value="progress"></progress>
    </div>
</template>

Let’s examine the code step by step and observe the bigger pieces:

When the user selects a file via the file input, the @change event will call the select method. In the select method, we set the file in the data props so we can reuse it easily.

In the createChunks method, we define the size of one chunk in kilobytes and based on that we calculate the number of the chunks. Then we create the chunks by slicing the file with the proper offsets. We push the chunks it a data property, so we can upload them one by one.

Now let’s take a look at the formData computed property. Since we are not sending a simple JSON but a real file to the back-end, we need to prepare our data as a FormData instance. We append the file to the dataset using a .part suffix at the file name. Also, we indicate if the current chunk is the last one or not. This is needed because we can perform different actions on the back-end based on this.

The rest is very simple. We add the uploaded chunk’s size to the uploaded property and we calculate to total progress based on the original file’s size. Also, if the upload was successful, we remove the chunk from the array, which triggers the watcher that starts to upload the next chunk until we have any.

Handling Chunks on the Back-end

On the back-end, our job is really simple. Again, let’s see the code first, then the explanation comes.

class FilesController extends Controller
{
    ...

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $file = $request->file('file');

        $path = Storage::disk('local')->path("chunks/{$file->getClientOriginalName()}");

        File::append($path, $file->get());

        if ($request->has('is_last') && $request->boolean('is_last')) {
            $name = basename($path, '.part');

            File::move($path, "/path/to/public/someid/{$name}");
        }


        return response()->json(['uploaded' => true]);
    }

    ...
}
Make sure, the chunks directory exists on your local disk.

So, we accept the file and retrieve its chunk path where we will put the incoming chunks. We get the content of the chunk and append it to the existing one – if it does not exists it will be created. If the chunk is the last one – as we indicated in the form data before, we rename the file by removing the .part suffix. Then we move it to the desired place, for example to the public disk.

Note, we use File::move here to make it simple, but it’s possible you need to use the Storage facade to move it between disks properly.

After it’s done, our file is in one piece and moved to its place with its original name.

Summary

This is a very simple implementation of chunked file uploads, but the goal is to give some indication of how to achieve this safer approach.

From this point, it’s easy to implement a logic that handles multiple files at the same time, retry logic, pausing and so on.

Need a web developer? Maybe we can help, get in touch!