0
votes

I'm trying to upload a pdf to Firebase Storage using Firebase Cloud Functions, I have this post function with the following body:

{
    "email":"[email protected]",
    "name":"Gianni",
    "surname":"test",
    "cellphone":"99999999",
    "data": 
    {
    "file":BASE_64,
    "fileName":"test.pdf"
    }
}

I want to save the base64 value in the "file" field and name as "fileName" field, here is the function that saves the file:

const admin = require("firebase-admin");
  /**
   * Create a new file in the storage.
   * @param {Object} wrapper [File to upload in the storage.]
   * @param {String} path [Path to upload file to.]
   * @return {object} [Containing the response]
   */
  postStorageFileAsync(wrapper, path) {
    return new Promise((res, rej)=>{
      System.prototype.writeLog({
        wrapper: wrapper,
        path: path,
      });
      const newFile = admin.storage().bucket().file(wrapper.path);
      return newFile.save(wrapper.file).then((snapshot) => {
        System.prototype.writeLog({snap: snapshot});
        return snapshot.ref.getDownloadURL().then((downloadURL) => {
          System.prototype.writeLog({fileUrl: downloadURL});
          return res({code: 200, data: {url: downloadURL}});
        });
      }).catch((err)=>{
        System.writeLog(err);
        return rej(err);
      });
    });
  }

But I'm getting:

postCurriculum

Error: A file name must be specified. at Bucket.file (/workspace/node_modules/@google-cloud/storage/build/src/bucket.js:1612:19) at /workspace/Firebase/Firebase.js:43:48 at new Promise () at Object.postStorageFileAsync (/workspace/Firebase/Firebase.js:38:12) at /workspace/PersistanceStorage/PersistanceStorage.js:407:35 at processTicksAndRejections (internal/process/task_queues.js:97:5)

Aside for the error itself, does anybody has a working example/tutorial link on how to upload files to firebase storage through functions? The documentation is really lacking.

Thanks

2
It seems that wrapper.pathis null or undefined. Also, any special reason for not uploading the file directly to Cloud Storage (and if necessary trigger a Cloud Function to do any extra processing)?Renaud Tarnec
Hello Renauld and thanks for the answer, I printed the wrapper.path value and it's indeed populated, I'll search harder. Anyway I have no problems using the official APIs on the front-end, but I want to realize this "experiment" because I think this is a more correct approach: you can just make a single call to the REST endpoint vs 2 calls: (one to the firebase APIs to upload the file and get the url, one to your endpoints.), so actually minimizing resources use and possibly, transmission errors.Giulio Serra

2 Answers

1
votes

In addition to @Renaud's comment, Firebase Admin SDK relies on Cloud Storage client library to access storage buckets. If you check the Nodejs API doc, you'll see that getDownloadURL() does not exist so if you want to continue using the Admin SDK and get the download URL, you have to get the metadata of the file once it's uploaded.

Here's a sample code I came up with:

const admin = require("firebase-admin");
const os = require('os');
const fs = require('fs');
const path = require('path');

...

const wrapper = {
  "file" : "BASE_64",
  "fileName" : "filename.pdf",
}
const pathName = "test/dir"

const bucket = admin.storage().bucket();

async function postStorageFileAsync(wrapper, pathName) {
  // Convert Base64 to PDF. You're only allowed to write in /tmp on Cloud Functions
  const tmp = `${os.tmpdir()}/converted.pdf`
  fs.writeFileSync(tmp, wrapper.file, 'base64', (error) => {
    if (error) throw error;
  });   
  
  // Upload to GCS
  const target = path.join(pathName,wrapper.fileName);
  await bucket.upload(tmp, {
    destination: target
  }).then(res =>{
    fs.unlinkSync(tmp)
    console.log(`Uploaded to GCS`)
  })

  // Get Download URL
  const newFile =  await bucket.file(target); 
  const [metadata] = await newFile.getMetadata();
  const url = metadata.mediaLink;
  console.log(url);  
}

postStorageFileAsync(wrapper,pathName)

Note that in this code the object is not public, so unauthorized access to the URL will display a permission error. If you want to make your objects accessible to public, see File.makePublic().

0
votes

So based upon Donnald Cucharo answer I will post my working solution for the people like me that are still using the javascript engine (that does not support the async in method's signature) instead of the typescript engine:

Step 1) Install the @google-cloud/storage npm in your project, from what I understood every firebase storage is built on top of a google cloud storage instance.

Step 2) Follow this guide to generate an admin key from your firebase project console in order to upload it inside your functions folder to authorize your functions project to authenticate and comunicate with your storage project.

Step 3) Use the following code (it is very similar to Donnald Cucharo's solution but with heavy use of promises):

const functions = require("firebase-functions");
const path = require("path");
const os = require("os");

  
class System {
  
  ...

  /**
   * Extract th extension of a file from it's path, and returns it.
   * @param {String} directory [Dir from where to extract the extension from.]
   * @return {String} [Containing the extension of the file from the directory or undefined]
   */
  getFileExtensionFromDirectory(directory) {
    const ext = path.extname(directory);
    if (ext === "") {
      return undefined;
    }
    return ext;
  }

  /**
   * Extract the name of the file from a directory.
   * @param {String} directory [Dir from where to extract the extension from.]
   * @return {String} [Containing the filename]
   */
  getFileNameFromDirectory(directory) {
    const extension = this.getFileExtensionFromDirectory(directory);
    if (extension === undefined) return extension;
    return path.basename(directory, extension);
  }

  /**
   * Returns the system temp directory.
   * @return {String} [Containing the system temporary directory.]
   */
  getSystemTemporarydirectory() {
    return os.tmpdir();
  }
   
}

module.exports = System;

then, it's time for the actual uploading of the file:

const admin = require("firebase-admin");
const functions = require("firebase-functions");
const bucket = admin.storage().bucket(functions.config().bucket.url);
const System = require("../System/System");
const fs = require("fs");

/**
 * Pure fabrication class to access the Firebase services
 */
class Firebase {

  /**
   * Create a new file in the storage.
   * @param {Object} wrapper [File to upload in the storage.]
   * @param {String} path [Path to upload file to.]
   * @return {object} [Containing the response]
   */
  postStorageFileAsync(wrapper) {
    return new Promise((res, rej)=>{
      if (wrapper.file === undefined) {
        return rej({code: 400, error:
          {it: "Devi forninre il valore in base64 del file che vuoi caricare.",
            en: "You must provide the base64 value of the file to upload."}});
      }


      if (wrapper.path === undefined) {
        return rej({code: 400, error:
          {it: "Devi fornire il percorso dove vuoi caricare il tuo file.",
            en: "Missing path filed in wrapper."}});
      }

      const fileName = System.prototype.getFileNameFromDirectory(wrapper.path);
      const fileExtension = System.prototype.getFileExtensionFromDirectory(wrapper.path);

      if (fileName === undefined || fileExtension === undefined) {
        return rej({code: 400, error:
          {it: "Formato del file non riconosciuto.",
            en: "Unrecognized file type or file name, the file should be in fileName.extension format"}});
      }

      const file = fileName + fileExtension;
      const tmpDirectory = System.prototype.getSystemTemporarydirectory() + "/" + file;

      return fs.promises.writeFile(tmpDirectory, wrapper.file, "base64").then(()=>{
        const options = {
          destination: wrapper.path,
        };

        return bucket.upload(tmpDirectory, options).then(() =>{
          fs.unlinkSync(tmpDirectory);
          return this.getStorageFileFromPathAsync(wrapper.path).then((response)=>{
            return res(response);
          });
        });
      }).catch((err)=>{
        fs.unlinkSync(tmpDirectory);
        System.prototype.writeLog(err);
        return rej({code: 500, error: err});
      });
    });
  }

  /**
   * Retrieve the url of a file in the storage from the path.
   * @param {String} path [Path of the file to get the url]
   * @return {Object} [Containing te response]
   */
  getStorageFileFromPathAsync(path) {
    return new Promise((res, rej)=>{
      bucket.file(path).makePublic().then(()=>{
        bucket.file(path).getMetadata() .then((response)=>{
          System.prototype.writeLog({response: response});
          const metadata = response[0];
          System.prototype.writeLog({metadata: metadata});
          return res({code: 200, url: metadata.mediaLink});
        });
      }).catch((err)=>{
        System.prototype.writeLog(err);
        return rej({code: 500, error: err});
      });
    });
  }

}

module.exports = Firebase;

Now let's talk about the code, because there were some parts that I've struggle to understand, but the main reasoning is something like this:

  1. We need to create a directory with an empty file in the temp directory of the system.

  2. By using the fs library we need to open a stream and dump the data in the temp directory we created:

    fs.promises.writeFile(tmpDirectory, wrapper.file, "base64").then(()=>{ ....})

  3. We can now upload the file in the firebase storage by using the path we created in step 1-2 but we can't access the download url yet.

  4. As we authenticated as admins before we can just change the newly created file visibility and finally get the download url by using the getStorageFileFromPathAsync() method.

I hope it was helpful.