Login

Firebase Storage User Profile Image

J
Jonathan Gamble
Published 13 min read

Many database platforms handle authentication with the database functions, but not many have built-in storage capabilities. Firebase Storage allows you to securely and easily upload files in buckets, similar to folders.

Firebase Storage Profile Image

TL;DR#

We use Svelte to update a user’s photoURL in the Firebase Authentication database. The image is stored in Firebase Storage, the old image is deleted automatically, and only image types are allowable.

Svelte Example#

If you read many articles on this site, you should know now that I use Svelte for easy prototyping. This app is no exception. However, the concept is the same in any framework.

Edit Image Component#

I use a custom image hook that returns a status and a files writable. In other frameworks, this would use signals instead of stores. You may also use a form and submit this to the backend.

HTML
<script lang="ts">
	import { useUploadImage } from '$lib/use-upload-image';
	import Card from '@components/elements/card.svelte';

	const { status, files } = useUploadImage();
</script>

<div class="my-5 flex items-center justify-center">
	<div class="w-3/4 max-w-3xl">
		<Card class="flex flex-col gap-5">
			<h1 class="text-3xl font-bold">Upload Image</h1>
			<input type="file" bind:files={$files} accept="image/*" />
			{#if $status.progress}
				<p>Percent: {$status.progress.toString()}</p>
			{/if}
			{#if $status.error}
				<p class="text-red-600">
					<span class="font-bold">Error: </span>
					{$status.error}
				</p>
			{/if}
		</Card>
	</div>
</div>

Image Type#

You can tell an HTML Input Element what you want to accept. For any image type:

TypeScript
<input type="file" bind:files={$files} accept="image/*" />

For specific MIME types:

TypeScript
<input type="file" accept="image/png, image/webp, image/jpeg, image/jpg, image/gif" />

Or by extension:

TypeScript
<input type="file" accept=".png, .webp, .jpg, .jpeg, .gif">

We will need to secure this later in Firebase Storage Rules.

Upload Image Hook#

The hook looks complicated.

TypeScript
import { auth, storage } from "./firebase";
import {
    derived,
    writable,
    type Writable
} from "svelte/store";
import {
    deleteObject,
    getDownloadURL,
    ref,
    uploadBytesResumable,
    type TaskState
} from "firebase/storage";
import { updateProfile } from "firebase/auth";

type UploadState = {
    progress: number | null;
    state: TaskState | null;
    error: string | null;
    downloadURL: string | null;
}

export const useUploadImage = () => {

    const files = writable<FileList>();

    const status = derived<
        Writable<FileList>,
        UploadState
    >(files, ($files, set) => {

        if (!$files) {
            set({
                progress: null,
                state: null,
                error: null,
                downloadURL: null
            });
            return;
        }

        if (!auth.currentUser) {
            throw 'Not logged in!';
        }

        const uid = auth.currentUser.uid;

        const new_file = $files[0];

        if (new_file.size >= 1 * 1024 * 1024) {
            set({
                progress: null,
                state: null,
                error: 'Image size must be less than 1MB!',
                downloadURL: null
            })
            return;
        }

        const uploadTask = uploadBytesResumable(
            ref(storage, `profiles/${uid}/${new_file.name}`),
            new_file
        );

        uploadTask.on('state_changed',
            (snapshot) => {

                // handle upload progress
                const progress = (
                    snapshot.bytesTransferred / snapshot.totalBytes
                ) * 100;
                set({
                    progress,
                    state: snapshot.state,
                    error: null,
                    downloadURL: null
                });

            },
            (error) => {

                // error handling
                set({
                    progress: null,
                    state: 'error',
                    error: error.message,
                    downloadURL: null
                })
            },
            () => {

                // success, get download URL
                getDownloadURL(uploadTask.snapshot.ref)
                    .then((downloadURL) => {

                        // delete current image
                        deleteImage().then(() => {
                            if (!auth.currentUser) {
                                throw 'No User!';
                            }
                            // update profile with new image
                            updateProfile(auth.currentUser, {
                                displayName: auth.currentUser.displayName,
                                photoURL: downloadURL
                            }).then(() => {

                                // set progress to 100%
                                set({
                                    progress: 100,
                                    state: 'success',
                                    error: null,
                                    downloadURL
                                });
                            });
                        })

                    });
            }
        );

    });

    return {
        files,
        status
    };
};

File Type#

With an input file element, you can keep track of the file being uploaded with a FileList type. Since we are only uploading one file, the file will be the first in the array.

TypeScript
const new_file = $files[0];

We can limit the file size to 1MB for our client. This should be matched on the backend as well.

TypeScript
new_file.size >= 1 * 1024 * 1024

Upload Part#

Let’s break down the upload part first.

TypeScript
const uploadTask = uploadBytesResumable(
    ref(storage, `profiles/${uid}/${new_file.name}`),
    new_file
);

We upload to the profiles/{userId}/{filename} folder here. The file name can be anything as long as it is in the user’s folder.

Progress#

We can keep track of the upload in real-time using uploadTask.on.

TypeScript
uploadTask.on('state_changed',
    (snapshot) => {

        // handle upload progress
        const progress = (
            snapshot.bytesTransferred / snapshot.totalBytes
        ) * 100;
        // handle progress

    },
    (error) => {

        // error handling
        
    },
    () => {

        // success, get download URL
        getDownloadURL(uploadTask.snapshot.ref)
            .then((downloadURL) => {

                // handle download URL
                // set progress to 100%

            });
    }
);

We can translate this to an observable, a signal, or a store (in the case of Svelte).

Delete Image#

In some cases, we may want to allow the user to have only one profile image at a time. Here, we remove the old profile image.

TypeScript
deleteObject(
    ref(storage, photoURL)
);

Update the User Profile#

We also need to update the user’s profile once we receive an image, upload a new image, or delete an image. The linked article explains this more fully.

Storage Rules#

Of course, we should secure our app on the backend as well.

TypeScript
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
  	match /profiles/{userId}/{image} {
    	allow read;
      allow create: if isLoggedIn() 
      	&& isOwner()
        && isImageType()
        && oneMBLimit();
      allow delete: if isLoggedIn()
        && isOwner();
    }
  }
  
  function isOwner() {
  	return request.path[4] == request.auth.uid;
  }
  
  function isLoggedIn() {
  	return request.auth != null;
	}
  
  function isImageType() {
  	return request.resource.contentType.matches('image/.*');
	}
  
  function oneMBLimit() {
    return request.resource.size < 1 * 1024 * 1024;
  }
  
}

Our path will be the storage file path instead of the collection path. For a more in-depth discussion of rules, see the Firestore Security Rules Example Guide. You may need some advanced rules. See Security Rules for Cloud Storage Reference.

Image Best Practices#

  • If your images could be used in multiple places, you may not want to ever delete them. Images are cheap to store and are hosted on a CDN.
  • However, if an image could be an orphan (with no links to it), it is best practice to delete it automatically to save yourself in the long run.
  • Your image should have good keywords in the file name for best SEO practices. I don’t do that here, but it is good to know.
  • ⚠️ Always use a new name when replacing an image. Images are cached by default, and it is extremely hard to reset the browser, server, and CDN caches when you update an image.
    • Images belonging to a user should go in that user’s folder. Ex: profiles/{userId}
    • You could generate a random UUID for every image or use a date timestamp.
    • You could keep the keywords with the random string. Ex: profiles/{userId}/firebase-functions-image-x2030s0eksl.jpg.

Firebase Function Storage Triggers#

There are Cloud Storage Trigger Functions that allow you to handle different events. I will only cover Generation 2, which you should use for speed and security.

  • onObjectArchived - Used when versioning is enabled to archive old versions of files.
  • onObjectDeleted - Used when a file is deleted.
  • onObjectFinalized - Used when a file has finished uploading, even when replacing old files.
  • onMetadataUpdated - Used when metadata changes.

Usage#

Relating to this app, you could use a storage trigger function to guarantee that the old user profile image is deleted or that the photoURL is updated.

  • ⚠️ CAVEAT: When the user profile image is updated on the server (in a server function or Cloud Function), it will not get updated until the active user logs out and back in. You could redundantly update it, but in a real app you would probably use Cloud Firestore to store the profile image. See Update a User Profile in Firebase.

In a production app, the best bet is to update Cloud Firestore inside the trigger function with the new download URL.

Demo: Vercel Serverless

Repo: GitHub

J

Comments

Share a thought or join the conversation.

Sign In to comment or reply.

Newsletter

Get updates on future FREE tutorials, paid courses, and new blog posts!

Tags

Explore posts by topic.