Introduction
Image Optimization is a common need for all apps. If you are using Firebase for storing images, Firebase makes it really easy to optimize images. Firebase provides an extension “Resize Images” for this purpose.
Setting up this extension is really easy. You have to install the extension in your desired Firebase project and configure the extension.
Note: To install Firebase extensions, your project must be on the Blaze plan (Pay as you go).
Extension configuration
The extension provides many configuration parameters that cover most of the needs including conversion to different formats. All the configuration parameters are very clearly documented on the extension page.
Missing piece
But one missing feature of this extension is it does not update the URL of the image with an optimized one if stored in the firestore. It is a very common scenario. As an example, suppose your Firestore database contains a collection for users. In documents of this collection, you are saving user profile photo. After optimizing images, it is important to update the old, large image URL with the new optimized image URL. Failure to do so would defeat the purpose of optimization, as users would still be receiving the unoptimized images. In the worst-case scenario, if you have selected the "Deletion of original file" option in the configuration parameter, it could result in broken images.
Solution
So, to update url of images in Firestore document, we have to write our own custom function. But, first, we need to make sure that we have “Enabled events” in the extension configuration. Due to this, when the optimized image is generated and saved, it will emit a custom event with the type "firebase.extensions.storage-resize-images.v1.complete".
Our custom function will be triggered in response to this event.
Now let’s shift our attention to the implementation of the function.
Pseudo code of function
Here are high-level steps for this function before digging into the code:
- Extract the path of the original and optimized image from event data.
- Get a reference of the original and optimized image.
- Get URL of both images from references
- Using the original image URL, find the id of the user
- Using id, update the user document with the optimized image URL (generated in step 3)
- Delete the original file if you want.
Note: Make sure to disable the “Deletion of original file” from the configuration parameters. The reason is the function makes use of the original image to locate the user and find the user id. If you want to delete the original image which in most cases you will, you can delete it inside the function.
Typescript code
Here’s the complete code (Make sure to update the collection and property name according to your database schema):
import * as admin from "firebase-admin";
import * as logger from "firebase-functions/logger";
import { onCustomEventPublished } from "firebase-functions/v2/eventarc";
import { getDownloadURL } from "firebase-admin/storage";
interface ImageResizedEvent {
input: {
selfLink: string;
crc32c: string;
contentDisposition: string;
md5Hash: string;
bucket: string;
generation: string;
timeStorageClassUpdated: string;
storageClass: string;
contentType: string;
updated: string;
etag: string;
metadata: {
firebaseStorageDownloadTokens: string;
};
name: string;
size: string;
metageneration: string;
kind: string;
timeCreated: string;
id: string;
mediaLink: string;
};
outputs: Output[];
}
interface Output {
size: string;
success: boolean;
outputFilePath: string;
}
if (!admin.apps.length) admin.initializeApp();
const findUserIdByAvatarURL = async (avatarURL: string) => {
try {
const querySnapshot = await admin
.firestore()
.collection("users")
.where("avatarURL", "==", avatarURL)
.get();
if (!querySnapshot.empty) {
const userId = querySnapshot.docs[0].id;
return userId;
}
return null;
} catch (error) {
logger.error("Error finding user by avatar URL", error);
throw error;
}
};
const updateAvatarURL = async (userId: string, newAvatarURL: string) => {
try {
const userRef = admin.firestore().collection("users").doc(userId);
await userRef.update({ avatarURL: newAvatarURL });
admin.auth().updateUser(userId, {photoURL: newAvatarURL})
} catch (error) {
logger.error("Error updating avatar URL", error);
throw error;
}
};
const deleteFile = async (filePath: string) => {
try {
await admin.storage().bucket().file(filePath).delete()
logger.info(`File ${filePath} deleted successfully.`);
} catch (error) {
logger.error(`Error deleting file ${filePath}:`, error);
throw error;
}
};
const updateDriverAvatarUrlOnResize = onCustomEventPublished<ImageResizedEvent>(
"firebase.extensions.storage-resize-images.v1.complete",
async (event) => {
try {
const { name: orgImagePath, generation } = event.data.input;
const { outputFilePath: optimizedImagePath } = event.data.outputs[0];
const orgImageRef = admin.storage().bucket().file(orgImagePath, {
generation,
});
const optimizedImageRef = admin
.storage()
.bucket()
.file(optimizedImagePath);
const orgImageUrl = await getDownloadURL(orgImageRef);
const optimizedImageUrl = await getDownloadURL(optimizedImageRef);
const userId = await findUserIdByAvatarURL(orgImageUrl);
if (userId) {
await updateAvatarURL(userId, optimizedImageUrl);
await deleteFile(orgImagePath);
} else {
logger.warn("User not found for avatar URL:", orgImageUrl);
}
} catch (error) {
logger.error("Error processing image resize event", error);
}
}
);
export default updateDriverAvatarUrlOnResize;
Fun Fact: I have not typed the whole long ImageResizedEvent interface. I have just logged the event object to logs explorer, copy it and ordered chatgpt to write interface for this object.
That’s all for this blog post. Hope you have enjoyed it. Happy coding.
Top comments (0)