Uploading Multiple Files with Fetch

Uploading Multiple Files with Fetch

This afternoon I was going through my "blog ideas" list and cleaning up entries I've changed my mind on. I came across something I added many months ago - using the Fetch API to upload multiple files at once. The reason it's stuck in my "todo" pile for so long is that I wasn't aware of a good service I could use to post my files against. I've done it before in Node.js and I know it's something I could do locally in an hour, but honestly I just didn't want to. That probably sounds a bit lazy but it's honest. Today though I came across httpbin.org, an online service that lets you hit it with various types of HTTP methods and even supports file uploads. (Obviously it doesn't make those files available, it just reports back on the upload.) Even better, it supports CORS which means I could use CodePen. So with no more excuses at my disposal, today I finally built a simple demo.

First off, I created a simple form:

<form>
	<input id="filesToUpload" type="file" multiple>
	<button id="testUpload">Test Upload</button>
</form>

<div id="status"></div>

I've got a file field, a button, and an empty div. Notice the file field uses the multiple attribute. This lets the end user select one or more files. For my first iteration, I used the following JavaScript:

document.addEventListener('DOMContentLoaded', init, false);

let fileField, statusDiv;

async function init() {
	fileField = document.querySelector('#filesToUpload');
	statusDiv = document.querySelector('#status');
	document.querySelector('#testUpload').addEventListener('click', doUpload, false);
}

async function doUpload(e) {
	e.preventDefault();
	statusDiv.innerHTML = '';

	let totalFilesToUpload = fileField.files.length;
	
	//nothing was selected 
	if(totalFilesToUpload === 0) {
		statusDiv.innerHTML = 'Please select one or more files.';
		return;
	}

	for(let i=0;i<totalFilesToUpload; i++) {
		statusDiv.innerHTML = `Working on file ${i+1} of ${totalFilesToUpload}`;
		let resp = await uploadFile(fileField.files[i]);
		console.log(`Done with ${i+1} item.`);
	}
	
	statusDiv.innerHTML = 'All complete.';
	fileField.value='';
}

async function uploadFile(f) {
	let form = new FormData();
	form.append('file', f);	
	let resp = await fetch('https://httpbin.org/post', { method: 'POST', body:form });
	let data = await resp.json();
	//console.log(data);
	return data;
}

From top to bottom - I begin by using querySelector to cache access to my file field and empty div. Then I add a click handler to the button.

The click handler first checks to see if any files were selected. If none were then we print out a message and leave. Otherwise, we then iterate over the files array and call an async function, uploadFile. In my demo, uploadFile does a POST to httpbin and returns the result. Right now I'm ignoring the result but in a real application you would probably need something from there. At the end of each upload I update my div with a status.

Finally I report that everything is complete and reset the file field. Here's a CodePen for you to try it out yourself:

See the Pen fetch multi sequential by Raymond Camden (@cfjedimaster) on CodePen.

This works well, but uploads the files one after the other. It would be nicer if they were all uploaded at once, right? Here's an updated version that does that:

document.addEventListener('DOMContentLoaded', init, false);

let fileField, statusDiv;

async function init() {
	fileField = document.querySelector('#filesToUpload');
	statusDiv = document.querySelector('#status');
	document.querySelector('#testUpload').addEventListener('click', doUpload, false);
}

async function doUpload(e) {
	e.preventDefault();
	statusDiv.innerHTML = '';

	let totalFilesToUpload = fileField.files.length;
	
	//nothing was selected 
	if(totalFilesToUpload === 0) {
		statusDiv.innerHTML = 'Please select one or more files.';
		return;
	}

	statusDiv.innerHTML = `Uploading ${totalFilesToUpload} files.`;

	let uploads = [];	
	for(let i=0;i<totalFilesToUpload; i++) {
		uploads.push(uploadFile(fileField.files[i]));
	}
	
	await Promise.all(uploads);
	
	statusDiv.innerHTML = 'All complete.';
	fileField.value='';
}

async function uploadFile(f) {
	console.log(`Starting with ${f.name}`);
	let form = new FormData();
	form.append('file', f);	
	let resp = await fetch('https://httpbin.org/post', { method: 'POST', body:form });
	let data = await resp.json();
	console.log(`Done with ${f.name}`);
	return data;
}

The main difference is tht now I don't await the call to uploadFile and use the implied Promise returned instead. I can then use Promise.all on the array of uploads to notice when they are all done. One thing I don't have is the nice "X of Y" message, and that's possibly something I could do too, but for now the improved speed should be nice. If you want to test this version, it's below.

See the Pen fetch multi sequential by Raymond Camden (@cfjedimaster) on CodePen.

Enjoy, let me know what you think!

Photo by Mia Anderson on Unsplash

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com