Redimensionar imágenes con Firebase Functions

July 17, 2019 • ☕️ 4 minutos de lectura

Hace poco empecé a usar firebase para acelerar un poco el trabajo, pensé que usándolo al menos no tendría que montar servidores, crear sistema de autentificación.

El caso es que al final tuve que probar las funciones de firebase, un ejemplo fué crear miniaturas de las imágenes que se suben.

Lo primero es instalar la herramienta firebase-tools

npm install -g firebase-tools

Ahora debemos hacer login si no lo habíamos hecho anteriormente.

firebase login

Iniciamos el proyecto

firebase init

Ahora debemos escoger la tercera opción que es la que nos interesa. Functions.

Captura de pantalla firebase init”

En este punto debemos escoger el proyecto que queremos usar o bien nos da la opción de crear uno nuevo.

Captura de pantalla firebase init”

Captura de pantalla firebase init”

Captura de pantalla firebase init”

Captura de pantalla firebase init”

Captura de pantalla firebase init”

Ahora denemos que editar el fichero /functions/index.js y añadir nuestro código, en este caso queremos redimensionar una imagen.

Si quieres más información de como activar otros triggers de firebase, te recomiendo este artículo

"use strict"

const functions = require("firebase-functions")
const mkdirp = require("mkdirp-promise")
const admin = require("firebase-admin")
admin.initializeApp()
const firestore = admin.firestore();
const spawn = require("child-process-promise").spawn
const path = require("path")
const os = require("os")
const fs = require("fs")

// Max height and width of the thumbnail in pixels.
const THUMB_MAX_HEIGHT = 400
const THUMB_MAX_WIDTH = 400
// Thumbnail prefix added to file names.
const THUMB_PREFIX = "thumb_"

/**
 * Cuando subimos una imagen al Storage bucket, generamos una miniatura automáticamente usando ImageMagick.
 * Después de que la miniatura se haya generado y cargado en Cloud Storage,
 * escribimos la URL pública en la base de datos en firestore.
 */
exports.generateThumbnail = functions.storage
  .object()
  .onFinalize(async object => {
    // Fichero y rutas de directorio.
    const filePath = object.name
    const contentType = object.contentType // This is the image MIME type
    const fileDir = path.dirname(filePath)
    const fileName = path.basename(filePath)
    const thumbFilePath = path.normalize(
      path.join(fileDir, `${THUMB_PREFIX}${fileName}`)
    )
    const tempLocalFile = path.join(os.tmpdir(), filePath)
    const tempLocalDir = path.dirname(tempLocalFile)
    const tempLocalThumbFile = path.join(os.tmpdir(), thumbFilePath)

    // Cerrar si es un archivo que no es una imagen
    if (!contentType.startsWith("image/")) {
      return console.log("This is not an image.")
    }

    // Cerrar si ya existe el thumbnail.
    if (fileName.startsWith(THUMB_PREFIX)) {
      return console.log("Already a Thumbnail.")
    }

    // Cloud Storage files.
    const bucket = admin.storage().bucket(object.bucket)
    const file = bucket.file(filePath)
    const thumbFile = bucket.file(thumbFilePath)
    const metadata = {
      contentType: contentType,
      // Para habilitar el almacenamiento en caché del lado del cliente, puede configurar los encabezados de Cache-Control aquí. Descomentar abajo.
      // 'Cache-Control': 'public,max-age=3600',
    }

    // Create the temp directory where the storage file will be downloaded.
    await mkdirp(tempLocalDir)
    // Download file from bucket.
    await file.download({ destination: tempLocalFile })
    console.log("The file has been downloaded to", tempLocalFile)
    // Generar un thumbnail usando ImageMagick.
    await spawn(
      "convert",
      [
        tempLocalFile,
        "-thumbnail",
        `${THUMB_MAX_WIDTH}x${THUMB_MAX_HEIGHT}>`,
        tempLocalThumbFile,
      ],
      { capture: ["stdout", "stderr"] }
    )
    console.log("Thumbnail created at", tempLocalThumbFile)
    // Subiendo el Thumbnail.
    await bucket.upload(tempLocalThumbFile, {
      destination: thumbFilePath,
      metadata: metadata,
    })
    console.log("Thumbnail uploaded to Storage at", thumbFilePath)
    // Una vez que la imagen se haya cargado, borramos los archivos locales para liberar espacio en el disco.
    fs.unlinkSync(tempLocalFile)
    fs.unlinkSync(tempLocalThumbFile)
    // Obtenga las URL ara la miniatura y la imagen original.
    const config = {
      action: "read",
      expires: "03-01-2500",
    }
    const results = await Promise.all([
      thumbFile.getSignedUrl(config),
      file.getSignedUrl(config),
    ])
    console.log("Got Signed URLs.")
    const thumbResult = results[0]
    const originalResult = results[1]
    const thumbFileUrl = thumbResult[0]
    const fileUrl = originalResult[0]
    // Añadir las URLs a la base de datos firestore
    let data = {
        path: fileUrl, thumbnail: thumbFileUrl
      };
      
      // Add a new document in collection "cities" with ID 'LA'
      let setDoc = firestore.collection('thumbs').add(data);

    return console.log("Thumbnail URLs saved to database.")
    
  })

Si quisiesemos añadirlo a firebase real time data base, podríamos usar lo siguiente.

// Añadir las URLs a la base de datos
    await admin
      .database()
      .ref("thumbs")
      .push({ path: fileUrl, thumbnail: thumbFileUrl })
    return console.log("Thumbnail URLs saved to database.")

Ahora instalaremos las dependencias necesarias para nuestro pequeña función. Recuerda que esto debemos hacemos desde el directorio functions.

cd functions/
npm i mkdirp-promise
npm i child-process-promise
npm i path
npm i os
npm i fs
npm install

Implementamos la funcion en firebase, para ello usaremos.

firebase deploy

Captura de pantalla firebase init”

En este punto, tenemos la función corriendo, pero nos devolverá el siguiente error.

Error: A Forbidden error was returned while attempting to retrieve an access token for the Compute Engine built-in service account. This may be because the Compute Engine instance does not have the correct permission scopes specified. Permission iam.serviceAccounts.signBlob is required to perform this operation on service account projects/my-app/serviceAccounts/my-app@appspot.gserviceaccount.com.

Captura de pantalla firebase init”

Esta parte de tener que saber moverte por las diferentes plataformas, acaba cansándome 😩, pero no podemos olvidarnos de ella.

Ve a tu página de administración y IAM de Google Cloud Platform. Verás varias cuentas de servicio. Busca la cuenta de servicio que aparece en el error de la consola. my-app@appspot.gserviceaccount.com. Selecciónalo y pulsa el icono del lápiz para editarlo y añadirle un nuevo ROLE, en este caso Creador de tokens de cuenta de servicio

Podemos comprobar que todo está bien de la consola de firebse, entrando en funciones y registro, deberíamos ver algo así.

Captura de pantalla firebase init”

Ahora ya tenemos nuestra función y podrá generar las miniaturas y poder almacenarlas en nuestra base de datos.