1

Getting serious with  Firebase V9.  Part 4 - Cloud Storage: Uploading files

 2 years ago
source link: https://dev.to/mjoycemilburn/getting-serious-with-firebase-v9-part-4-cloud-storage-uploading-files-3p7c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
MartinJ

Posted on Dec 19

Getting serious with  Firebase V9.  Part 4 - Cloud Storage: Uploading files

Introduction

File upload to the server host is a common webapp requirement. For example, you might want users of a Blog app to be able to add video files to their posts.

Previously, you've seen how the Firebase deploy procedure can be used to upload static assets to the server. But here we're talking about dynamic assets. A Firebase deploy isn't going to be of any use in this situation. Google's answer to this requirement is a service called "Firebase Storage".

If you were observant, you might have caught sight of this when you first saw the Firebase Console in Firebase project configuration. At the time, attention was focused on Authentication, Firestore and Hosting but, included in the list of "Build" tabs in the left-hand column you might have spotted a service labelled "Storage".

Open the Console Storage page for your project now, find the "Storage" tab and click it.

Cloud Storage is actually a huge part of the Google Cloud system - you can get a glimpse of Google's overall vision for the service at Cloud Storage for Firebase. I'll be using just a fraction of its capabilities in this post, but you'll quickly see that this is a facility that you can rely upon to meet all of your requirements for robust, scalable and secure storage.

Cloud storage is organised around storage buckets. This is how Google itself described the system:

Buckets are the basic containers that hold your data. Everything that you store in Cloud Storage must be contained in a bucket. You can use buckets to organize your data and control access to your data, but unlike directories and folders, you cannot nest buckets. While there is no limit to the number of buckets you can have in a project or location, there are limits to the rate you can create or delete buckets.

When you create a bucket, you give it a globally-unique name and a geographic location where the bucket and its contents are stored. The name and location of the bucket cannot be changed after creation, though you can delete and re-create the bucket to achieve a similar result. There are also optional bucket settings that you can configure during bucket creation and change later.

The first time you open the Console's Storage page, Google will ask you to initialise the default Storage "bucket" that has been allocated to your project (you can see the name of this if you open Project Settings and look for "storageBucket")

Initialisation is generally quite straightforward but you may be slightly thrown when you are asked whether you want to start your project in Test or Production mode. You might remember, however, that there was something similar when you were initialising your project's Firestore settings - it has to do with storage rules. At this stage, you should select "test" - more on this later. Select a suitable geographical location too - somewhere reasonably close should be your aim. Once you're through all this, the Storage page should look something like the following:

Uploading a file to Cloud Storage

An earlier post, Coding a simple webapp used a "Shopping List" application as an illustration. This allowed users to login and create lists of purchase intentions. I'm now going to show you how you might add a feature to this to enable users to add a profile icon to their accounts.

Incidentally, you might imagine that one approach to this, rather than using Cloud Storage, might be to store uploaded data in a Firestore collection. So, for example, you might create a userAccounts collection keyed on userEmail and then try to store an uploaded image file here in a data field formatted as some sort of encoded string. But this isn't going to work because the maximum size of a Firestore document is 1MB and images will generally be a lot larger than this. So the plan must be to upload the graphic into a file created dynamically in Cloud storage with a filename based on `userEmail.

The screenshot above shows the default fir-expts-app.appspot.com bucket allocated to the fir-expts-app project for the ShoppingLists project. Should I store the profile icon files directly in here or should I create a new bucket especially for this purpose? The Console certainly provides me with a facility to create buckets but you'll recall from Google's description of the bucket concept that these are not something to be treated quite as liberally as folders in local storage. The system does, however, allows me to freely create "folders" within buckets and this seems to be the direction that Google would prefer me to go - see Folders.

I've now got to write some Javascript to enable a user to select a profile graphic file from local storage and upload it. You'll probably be relieved that I'm not actually going to incorporate this into the original Shopping Lists application as the proposed enhancement is simply too absurd. In any case, it would obscure the techniques that I'm trying to explain. So if you want to follow me by trying this example out yourself, the code below is intended to overwrite the original index.html and index.js files in the firexptsapp project.

Here's the body of the demo index.html`:


<body style="text-align: center;">

    <input type="file" id="fileitem" accept="image/png, .txt, .pdf">
    <img id="imageitem" style="display: none; max-width: 20%; margin: 10vh auto 5vh auto;">

    <script src="packed_index.js" type="module"></script>
</body>

Enter fullscreen mode

Exit fullscreen mode

If you're not familiar with HTML file input elements, check out Mozilla's documentation at <input type="file"> - these provide a very neat way launching a file-selection window and storing the user's choice in the DOM.

Here's the corresponding index.js:

import { initializeApp } from 'firebase/app';
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { getStorage, ref, uploadBytes } from 'firebase/storage';

const firebaseConfig = {
    apiKey: "AIzaSyAPJ44X28c .... 6FnKK5vQje6qM",
    authDomain: "fir-expts-app.firebaseapp.com",
    projectId: "fir-expts-app",
    storageBucket: "fir-expts-app.appspot.com",
    messagingSenderId: "1070731254062",
    appId: "1:10707312540 ..... 61bd95caeacdbc2bf",
    measurementId: "G-Q87QDR1F9T"
};
const firebaseApp = initializeApp(firebaseConfig);

const provider = new GoogleAuthProvider();
const auth = getAuth();

const userEmail = "[email protected]"

const storage = getStorage();
const storageRef = ref(storage, 'userProfileIcons/'+ userEmail);

window.onload = function () {
    document.getElementById('fileitem').onchange = function () { uploadFile() };
}

function uploadFile() {
    var file = document.getElementById('fileitem').files[0];
    uploadBytes(storageRef, file).then((snapshot) => {
        alert('Successful upload');
    });
}

Enter fullscreen mode

Exit fullscreen mode

The uploadFile() function in the index.js code above is triggered when it sees the contents of the index.html's fileitem field change, signalling that the user has selected a file for upload. Prior to that you'll see that I've initialised and authorised the webapp exactly as before but then have used a couple of new functions imported from a firebase/storage module to create "storage references". These indicate where I want my uploaded file to end up. Here I've cheated a little by hard-coding a suitable userEmail field ([email protected]) into the webapp. If you were actually using a Google log-in, this would be used to supply the user's email.

The uploadFile() function itself is very simple. It just copies the selected file's details from the DOM entry for the HTML input field (files[0] means "get the details for the first in the list of selected files" - in this case, there's only one) and then supplies this (together with the storageRef field specifying the upload target) as parameters to the SDK's uploadBytes function. When this completes, it displays an alert message on the browser screen.

To put this into operation, I simply open my build_for_development.ps1 script file, select its contents and press F8. Once the code has been successfully "webpacked" (essential now because I'm using the modular V9 Firebase SDK) and deployed, I can launch the webapp with the supplied url (https://fir-expts-app.web.app in this case). If I then select a random .png file, the browser should respond with the expected 'Successful upload' alert message. And if I refresh the Firestore console's Storage page I should see that a userProfileIcons folder has been created and that my local file has been uploaded into this as [email protected]. I can check that it's the correct file by clicking on its console entry and noting the useful thumbnail and metadata that are then revealed.

Note that I don't need to actually create the userProfileIcons folder and if I run the webapp again selecting a different source file, the target is simply overwritten.

Referencing a file in Cloud Storage

Although I've now successfully created a Cloud copy of the local file, I haven't shown you how a webapp might actually use this to display the user's profile icon on the screen.

This is surprisingly easy. Another Cloud Storage function, getDownloadURL() takes a "storage reference" parameter and returns a url. It's then a simple matter to plug that into the src property of an <img> tag and thus display the Cloud Storage file on the browser screen. In some cases, you might find it useful/necessary to store these urls as string fields somewhere in a Firestore collection.

Here's the code I need to add to achieve this. First, in the body of the html file, I've added the following:

    <img id="imageitem" style="display: none; max-width: 20%; margin: 10vh auto 0 auto;">

Enter fullscreen mode

Exit fullscreen mode

and in the index.js file I've reworked the uploadFile() function as follows:

function uploadFile() {
    var file = document.getElementById('fileitem').files[0];
    uploadBytes(storageRef, file).then((snapshot) => {
        alert('Successful upload');
        getDownloadURL(storageRef)
            .then((url) => {
                document.getElementById('imageitem').src = url;
                document.getElementById('imageitem').style.display = "block";
            })
    });
}

Enter fullscreen mode

Exit fullscreen mode

Once I've rerun the build script, the webapp will display a Cloud Storage copy of the locally-sourced file:

There are a lot of variations that you can play on this basic theme. The files object that the input tag creates in the browser's DOM has many useful properties. For example, document.getElementById('fileitem').files[0].type; enables me to detect a pdf file (type would be 'application/pdf"). In this case, a javascript statement along the lines of window.open(url, "_blank"); would display the Cloud Storage copy of the file under a new browser tab.

Storage Rules

There's one final wrinkle to be ironed out. Because project API keys are essentially public, Google Cloud Storage needs to be protected by the same sort of "rule" arrangement that you saw earlier with regard to Firestore document security.

You can see the rules currently in force by clicking the "rules" tab on the Firebase Console's Storage page. Because I initialised storage for the fir-expts-app project in "test" mode they'll look something like the following:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if
          request.time < timestamp.date(2022, 1, 17);
    }
  }
}

Enter fullscreen mode

Exit fullscreen mode

These are saying "allow anybody to do anything while the run-date is prior to 17th Jan 2022". I ran my initialise on 18th Dec, 2021, so Google was giving me a month to get myself sorted out. After this date, unless I've changed the rules myself, they'll deny access entirely until I fix things. Just for now, though, the "test" setting is fine.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK