Adding image uploads to a Material UI + Firebase progressive web app
Late last year I wrote an article describing how I use Google Sheets behind Google Cloud Functions to provide master data to my chosen web app stack. My example was a progressive web app that I built called Bar Tool. It’s a mobile-first app that helps mixology enthusiasts like me riff on classic drink recipes. I use a Google Sheet to hold a library of recipes and Firebase to store user data. One of the most important things about mixology is the “serve”: the presentation of the finished item.
It seemed a pity not to capture the pictures along with the recipes and ratings, so it was time to add photos for the classic recipes and an upload for user recipes!
Bar Tool uses my standard tool-kit with Google Material React bindings at the front end.
Storing files in Firebase
Most of my use of Firebase is the services for storing JSON objects. It turns out that the capabilities that Firebase provides also includes a handy API just for storing files in Google Cloud Storage. I usually use AWS’s S3 for plain file storage requirements, but I love it when something just works with an API that I have already rolled in… no new dependencies!
The API has a function for fetching a storage object, creating a reference, and then uploading to it. It follows a very similar pattern to fetching and manipulating JSON objects, which makes for straightforward coding. The following few lines are all that is required to upload a file selected by the user in the browser. It returns a snapshot object with a reference to a generated, publicly accessible URL for the stored file.
Getting a reference to an array of file objects in Material UI is not difficult. The approach suggested in the docs is to create an HTML input, hide it, then wrap a button with it. This code passes the file chosen by the user as the first element in the parameter to handleUpload()
.
Image validation
Letting users upload arbitrary files comes with great responsibility. My app only really has real estate for a 512px wide image, so there’s not much point in letting anyone upload anything bigger. We also only want images.
Firebase provides authorisation rules of the same style as for JSON storage, with the ability to restrict size and content type. I went with the following rules:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read: if request.auth != null;
allow write: if request.auth != null
&& request.resource.size < 512 * 1024
&& request.resource.contentType.matches('image/.*');
}
}
}
I explored auto-resizing uploaded images using the extension system that Firebase provides. This extension for resizing on write looked promising. Unfortunately I couldn’t find a way to configure it without changing the generated URL for the resized file though. This would have required a significant change to the logic in the app to find the new file and store the reference to it, so I decided to give it a miss. I went with the less pleasant user experience of raising an error if file.size
on the uploaded file is too large and asking the user to have another go. Something to improve another day.
Standard recipe images
There’s no need to use Firebase for my standard recipe library, so I added a column to my Google Sheet for image URLs and included the image files in the public image assets in my Create React App based front end app.