Ezra Butler
Ezra Butler

Reputation: 140

Uploading to Firebase Storage from a Google Cloud Function

I'm trying to create a Firebase Function that allows me to pass an array of image URLs in to create generate a montage, upload the file to Firebase Storage and then return the generated Download URL. This will be called from my app, so I'm using functions.https.onCall.

const functions = require("firebase-functions");
const admin = require('firebase-admin');
var gm = require('gm').subClass({imageMagick: true});
admin.initializeApp();


exports.createMontage = functions.https.onCall((data, context) => {
var storageRef = admin.storage().bucket( 'gs://xyz-zyx.appspot.com' );
var createdMontage = storageRef.file('createdMontage.jpg');
function generateMontage(list){
  let g = gm()
   list.forEach(function(p){
       g.montage(p);
   })
   g.geometry('+81+81')
   g.density(5000,5000)
   .write(createdMontage, function(err) {
       if(!err) console.log("Written montage image.");
   });
   return true
}

generateMontage(data)

return createdMontage.getDownloadURL();

});

The function generateMontage() works locally on NodeJs (with a local write destination).

Thank you.

Upvotes: 0

Views: 438

Answers (3)

Richard Zhan
Richard Zhan

Reputation: 500

I think you can pipe output stream from gm module to firebase storage object write stream.

const functions = require("firebase-functions");
const admin = require('firebase-admin');
var gm = require('gm').subClass({imageMagick: true});
admin.initializeApp();

exports.createMontage = functions.https.onCall(async (data, context) => {
   var storage = admin.storage().bucket( 'gs://xyz-zyx.appspot.com' );
   
   var downloadURL = await new Promise((resolve, reject) => {
         let g = gm()
         list.forEach(function(p){
             g.montage(p);
         })
         g.geometry('+81+81')
         g.density(5000,5000)
          .stream((err, stdout, stderr) => {
              if (err) {
                  reject();
              }
              stdout.pipe(
                  storage.file('generatedMotent.png).createWriteStream({
                    metadata: {
                        contentType: 'image/png',
                    },
                })
              ).on('finish', () => {
                storage
                    .file('generatedMotent')
                    .getSignedUrl({
                        action: 'read',
                        expires: '03-09-2491', // Non expring public url
                    })
                    .then((url) => {
                        resolve(url);
                    });
              });
         })
   });

   return downloadURL;
});

FYI, Firebase Admin SDK storage object does not have getDownloadURL() function. You should generate non-expiring public signed URL from the storage object.

In addition to, it should cause another problem after some period of time according to this issue. To get rid of this issue happening, you should initialize firebase app with permanent service account.

const admin = require('firebase-admin');
const serviceAccount = require('../your-service-account.json');

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    projectId: JSON.parse(process.env.FIREBASE_CONFIG).projectId,
    databaseURL: JSON.parse(process.env.FIREBASE_CONFIG).databaseURL,
    storageBucket: JSON.parse(process.env.FIREBASE_CONFIG).storageBucket,
});

Upvotes: 0

DazWilkin
DazWilkin

Reputation: 40061

Have a look at this example from the docs:

https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-code-sample

2021-01-11 Update

Here's a working example. I'm using regular Cloud Functions and it's limited in that the srcObject, dstObject and bucketName are constants but, it does create montages which is your goal.

PROJECT=[[YOUR-PROJECT]]
BILLING=[[YOUR-BILLING]]
REGION=[[YOUR-REGION]]

FUNCTION=[[YOUR-FUNCTION]]

BUCKET=[[YOUR-BUCKET]]
OBJECT=[[YOUR-OBJECT]] # Path from ${BUCKET} root

gcloud projects create ${PROJECT}

gcloud beta billing projects link ${PROJECT} \
--billing-account=${BILLING}

gcloud services enable cloudfunctions.googleapis.com \
--project=${PROJECT}

gcloud services enable cloudbuild.googleapis.com \
--project=${PROJECT}

gcloud functions deploy ${FUNCTION} \
--memory=4gib \
--max-instances=1
--allow-unauthenticated \
--entry-point=montager \
--set-env-vars=BUCKET=${BUCKET},OBJECT=${OBJECT} \
--runtime=nodejs12 \
--trigger-http \
--project=${PROJECT} \
--region=${REGION}

ENDPOINT=$(\
  gcloud functions describe ${FUNCTION} \
  --project=${PROJECT} \
  --region=${REGION} \
  --format="value(httpsTrigger.url)")

curl \
--request GET \
${ENDPOINT}


`package.json`:
```JSON
{
  "name": "montage",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/storage": "5.7.1",
    "gm": "^1.23.1"
  }
}

And index.js:

const { Storage } = require('@google-cloud/storage');
const storage = new Storage();

const gm = require('gm').subClass({ imageMagick: true });

const bucketName = process.env["BUCKET"];
const srcObject = process.env["OBJECT"];
const dstObject = "montage.png";

// Creates 2x2 montage
const list = [
  `/tmp/${srcObject}`,
  `/tmp/${srcObject}`,
  `/tmp/${srcObject}`,
  `/tmp/${srcObject}`
];

const montager = async (req, res) => {
  // Download GCS `srcObject` to `/tmp`
  const f = await storage
    .bucket(bucketName)
    .file(srcObject)
    .download({
      destination: `/tmp/${srcObject}`
    });

  // Creating GCS write stream for montage
  const obj = await storage
    .bucket(bucketName)
    .file(dstObject)
    .createWriteStream();

  let g = gm();
  list.forEach(f => {
    g.montage(f);
  });

  console.log(`Returning`);
  g
    .geometry('+81+81')
    .density(5000, 5000)
    .stream()
    .pipe(obj)
    .on(`finish`, () => {
      console.log(`finish`);
      res.status(200).send(`ok`);
    })
    .on(`error`, (err) => {
      console.log(`error: ${err}`);
      res.status(500).send(`uhoh!`);
    });
}
exports.montager = montager;

Upvotes: 2

Stratubas
Stratubas

Reputation: 3067

I have never used 'gm', but, according to its npm page, it has a toBuffer function.

So maybe something like this could work:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const gm = require('gm').subClass({ imageMagick: true });
admin.initializeApp();

exports.createMontage = functions.https.onCall((data, _context) => {
  const bucketName = 'xyz-zyx'; // not sure, I've always used the default bucket
  const bucket = admin.storage().bucket(bucketName);
  const storagePath = 'createdMontage.jpg';
  const fileRef = bucket.file(storagePath);
  const generateMontage = async (list) => {
    const g = gm();
    list.forEach(function (p) {
      g.montage(p);
    });
    g.geometry('+81+81');
    g.density(5000, 5000);
    return new Promise(resolve => {
      g.toBuffer('JPG', (_err, buffer) => {
        const saveTask = fileRef.save(buffer, { contentType: 'image/jpeg' });
        const baseStorageUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/`;
        const encodedPath = encodeURIComponent(storagePath);
        const postfix = '?alt=media'; // see stackoverflow.com/a/58443247/6002078
        const publicUrl = baseStorageUrl + encodedPath + postfix;
        saveTask.then(() => resolve(publicUrl));
      });
    });
  };
  return generateMontage(data);
});

But it seems it can be done more easily. As Methkal Khalawi commented:

here is a full example on how to use ImageMagic with Functions. Though they are using it for blurring an image but the idea is the same. And here is a tutorial from the documentation.

Upvotes: 0

Related Questions