197

After uploading a file in Firebase Storage with Functions for Firebase, I'd like to get the download url of the file.

I have this :

...

return bucket
    .upload(fromFilePath, {destination: toFilePath})
    .then((err, file) => {

        // Get the download url of file

    });

The object file has a lot of parameters. Even one named mediaLink. However, if I try to access this link, I get this error :

Anonymous users does not have storage.objects.get access to object ...

Can somebody tell me how to get the public download Url?

Thank you

Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
Valentin
  • 5,379
  • 7
  • 36
  • 50
  • See also [this post](https://stackoverflow.com/questions/44317062/database-triggers-firebase-function-to-download-images-from-url-and-save-it-to-s) which reconstructs the URL from data available in the function. – Kato Jun 14 '17 at 19:16
  • only if you have no firebase security rules i.e: allow read write on all conditions, then this pattern would be enough "https://firebasestorage.googleapis.com/v0/b/.appspot.com/o/?alt=media" – Inzamam Malik Oct 09 '20 at 14:03
  • signed url or token is only required if you have no readwrite permission if auth is null – Inzamam Malik Oct 09 '20 at 14:03

27 Answers27

184

You'll need to generate a signed URL using getSignedURL via the @google-cloud/storage NPM module.

Example:

const gcs = require('@google-cloud/storage')({keyFilename: 'service-account.json'});
// ...
const bucket = gcs.bucket(bucket);
const file = bucket.file(fileName);
return file.getSignedUrl({
  action: 'read',
  expires: '03-09-2491'
}).then(signedUrls => {
  // signedUrls[0] contains the file's public URL
});

You'll need to initialize @google-cloud/storage with your service account credentials as the application default credentials will not be sufficient.

UPDATE: The Cloud Storage SDK can now be accessed via the Firebase Admin SDK, which acts as a wrapper around @google-cloud/storage. The only way it will is if you either:

  1. Init the SDK with a special service account, typically through a second, non-default instance.
  2. Or, without a service account, by giving the default App Engine service account the "signBlob" permission.

Update (July 2023): A new getDownloadURL function was added to version 11.10 of the Firebase Admin SDK for Node.js. See the new documentation on creating a shareable URL or puf's answer.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
James Daniels
  • 6,883
  • 4
  • 24
  • 28
  • 110
    This is strange. We can easily get the Download Url from a Storage reference when using the Firebase Android, iOS and Web SDK. Why is it not as easy when being in admin? PS : Where can I find the 'service-account.json' needed to initialize gcs? – Valentin Mar 23 '17 at 08:26
  • 3
    This is because the admin-sdk does not have any Cloud Storage additions. You can get your admin-sdk service account json here https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk – James Daniels Mar 23 '17 at 17:46
  • None of the Cloud Functions for Firebase docs show initializing with service account credentials like that: https://github.com/firebase/functions-samples/blob/master/quickstarts/thumbnails/functions/index.js#L19-L21 However when I try without I get a 'SigningError' regarding the missing keyfile. @JamesDaniels – HowlingFantods Mar 30 '17 at 22:07
  • 1
    True, but none of the docs use getSignedUrl, something that I'll be fixing https://github.com/firebase/functions-samples/issues/82 According to Kato we do this in the codelab though. – James Daniels Apr 01 '17 at 02:21
  • 25
    URL generated with this method is ridiculously long. URL generated by @martemorfosis proposed method is much better. Is there any function that produce that URL? That's what I save in database for future use when I'm using firebase-sdk. A mirror method needs to exist in Functions domain. – Bogac Apr 19 '17 at 16:29
  • 3
    How can we upload the service-account.json along the deployed functions? I have tried placing it in the functions folder and referencing it in the file field in package.json but it is not getting deployed. Thank you. – David Aroesti Apr 25 '17 at 18:20
  • Wouldn't it be unsafe to leave the serviceAccount.json file in functions folder or somewhere in the project folder? – TheBen Oct 22 '17 at 18:28
  • Tell me please, how can I use firebase environment instead of json file in this case? – Alexander Khitev Oct 23 '17 at 05:23
  • 2
    Are we required to add `action` and `expires`? – Chad Bingham Jun 20 '18 at 16:40
  • 1
    @Alexander - You may need to look at Firebase.config for such environment variables – Abdul Vajid Sep 22 '18 at 10:32
  • I am still getting the following error. MalformedSecurityHeader Your request has a malformed header. signature
    Signature was not base64 encoded
    – raks Oct 27 '18 at 11:31
  • 1
    This answer is only partially updated, which caused a lot of confusion for me. See the solution posted by @SMX on 11/07/18 for detailed instructions on how to do this from the Firebase Admin SDK. – Kat Dec 12 '18 at 15:37
  • The service account you get from Firebase Console has "Editor" role, which doesn't have the necessary IAM permissions. You need a role like "Service Account Token Creator" for that. Check https://groups.google.com/forum/#!topic/firebase-talk/-kxUMVPjQkk – Dan Alboteanu Jan 29 '19 at 19:12
  • 3
    will this url get expired after a week? or it will be valid till 03-09-2491? @Frank van Puffelen – Jaydip Kalkani Jun 07 '19 at 11:54
  • I tried running `getSignedUrl` but the generated URL shows the following error: `Your request has a malformed header. Signature was not base64 encoded` Have posted the generated URL, workaround and full error here: https://github.com/googleapis/google-cloud-go/issues/1062#issuecomment-508970268 Is this a bug, or am I doing something wrong? – Mayur Dhurpate Jul 07 '19 at 04:47
  • 2
    This solution returns an insane long url that doesn't match what you get if you go into the firebase console and click get download url.. it also doesn't work – Ryan Langton Aug 06 '19 at 21:37
  • 3
    It doesn't work, I mean just for 7 days, is there a simple way to get a downloaded URL without expiration? – Dody Mar 18 '20 at 15:03
  • 13
    Beware! Signed URLs are not intended for long time usage, and they expire after 2 weeks at most (v4). If you intend to store this in your database for long time, Signed URLs are not the proper way . Check property `expires` here: https://googleapis.dev/nodejs/storage/latest/global.html#GetBucketSignedUrlConfig – maganap Apr 13 '20 at 14:28
  • 1
    I found this answer useful and I tried to upvote and guess what :-) the error message said "You last voted on this answer Oct 9 '20 at 14:03. Your vote is now locked in unless this answer is edited." google really need to simplify their docs for god sake – Inzamam Malik Jan 19 '21 at 12:59
  • Does this url works for long time use? I have tested and it's working till now(5days). I am not sure if this will work long time or not. – Abu Saeed Jan 24 '22 at 15:31
  • Error: Permission 'iam.serviceAccounts.signBlob' denied on resource (or it may not exist). I love web SDK. Admin SDK is crazy.. – Tom Fan Jul 02 '23 at 15:25
144

This answer will summarize the options for getting a download URL when uploading a file to Google/Firebase Cloud Storage. There are three types of download URLS:

  1. Token download URLs, which are persistent and have security features
  2. Signed download URLs, which are temporary and have security features
  3. Public download URLs, which are persistent and lack security

There are two ways to get a token download URL. Signed and public download URLs each have only one way to get them.

Token URL method #1: From the Firebase Storage Console

You can get the download URL from Firebase Storage console:

enter image description here

The download URL looks like this:

https://firebasestorage.googleapis.com/v0/b/languagetwo-cd94d.appspot.com/o/Audio%2FEnglish%2FUnited_States-OED-0%2Fabout.mp3?alt=media&token=489c48b3-23fb-4270-bd85-0a328d2808e5

The first part is a standard path to your file. At the end is the token. This download URL is permanent, i.e., it won't expire, although you can revoke it.

Token URL method #2: From the Front End

The documentation tells us to use getDownloadURL():

let url = await firebase.storage().ref('Audio/English/United_States-OED-' + i +'/' + $scope.word.word + ".mp3").getDownloadURL();

This gets the same download URL that you can get from your Firebase Storage console. This method is easy but requires that you know the path to your file, which in my app is difficult. You could upload files from the front end, but this would expose your credentials to anyone who downloads your app. So for most projects you'll want to upload your files from your Cloud Functions, then get the download URL and save it to your database along with other data about your file.

I can't find a way to get the token download URL when I write a file to Storage from a Cloud Function (because I can't find a way to tell the front end that a file has written to Storage), but what works for me is to write a file to a publicly available URL, write the publicly available URL to Firebase, then when my Angular front end gets the download URL from Firebase it also runs getDownloadURL(), which has the token, then compares the download URL in Firestore to the token download URL, and if they don't match then it updates the token download URL in place of the publicly available URL in Firestore. This exposes your file to the public only once.

This is easier than it sounds. The following code iterates through an array of Storage download URLs and replaces publicly available download URLs with token download URLs.

const storage = getStorage();
var audioFiles: string[] = [];

if (this.pronunciationArray[0].pronunciation != undefined) {
          for (const audioFile of this.pronunciationArray[0].audioFiles) { // for each audio file in array
            let url = await getDownloadURL(ref(storage, audioFile)); // get the download url with token
            if (audioFile !== url) { // download URLs don't match
              audioFiles.push(url);
            } // end inner if
          }; // end for of loop
          if (audioFiles.length > 0) { // update Firestore only if we have new download URLs
            await updateDoc(doc(this.firestore, 'Dictionaries/' + this.l2Language.long + '/Words/' + word + '/Pronunciations/', this.pronunciationArray[0].pronunciation), {
              audioFiles: audioFiles
            });
          }
} // end outer if

You're thinking, "I'll return the Storage location from my Cloud Function to my front end and then use the location with getDownloadURL() to write the token download URL to Firestore." That won't work because Cloud Functions can only return synchronous results. Async operations return null.

"No problem," you say. "I'll set up an Observer on Storage, get the location from the Observer, and then use the location with getDownloadURL() to write the token download URL to Firestore." No dice. Firestore has Observers. Storage doesn't have Observers.

"How about," you say, "calling listAll() from my front end, getting a list of all my Storage files, then calling the metadata for each file, and extracting the download URL and token for each file, and then writing these to Firestore?" Good try, but Storage metadata doesn't include the download URL or token.

Signed URL method #1: getSignedUrl() for Temporary Download URLs

getSignedUrl() is easy to use from a Cloud Function:

  function oedPromise() {
    return new Promise(function(resolve, reject) {
      http.get(oedAudioURL, function(response) {
        response.pipe(file.createWriteStream(options))
        .on('error', function(error) {
          console.error(error);
          reject(error);
        })
        .on('finish', function() {
          file.getSignedUrl(config, function(err, url) {
            if (err) {
              console.error(err);
              return;
            } else {
              resolve(url);
            }
          });
        });
      });
    });
  }

A signed download URL looks like this:

https://storage.googleapis.com/languagetwo-cd94d.appspot.com/Audio%2FSpanish%2FLatin_America-Sofia-Female-IBM%2Faqu%C3%AD.mp3?GoogleAccessId=languagetwo-cd94d%40appspot.gserviceaccount.com&Expires=4711305600&Signature=WUmABCZIlUp6eg7dKaBFycuO%2Baz5vOGTl29Je%2BNpselq8JSl7%2BIGG1LnCl0AlrHpxVZLxhk0iiqIejj4Qa6pSMx%2FhuBfZLT2Z%2FQhIzEAoyiZFn8xy%2FrhtymjDcpbDKGZYjmWNONFezMgYekNYHi05EPMoHtiUDsP47xHm3XwW9BcbuW6DaWh2UKrCxERy6cJTJ01H9NK1wCUZSMT0%2BUeNpwTvbRwc4aIqSD3UbXSMQlFMxxWbPvf%2B8Q0nEcaAB1qMKwNhw1ofAxSSaJvUdXeLFNVxsjm2V9HX4Y7OIuWwAxtGedLhgSleOP4ErByvGQCZsoO4nljjF97veil62ilaQ%3D%3D

The signed URL has an expiration date and long signature. The documentation for the command line gsutil signurl -d says that signed URLs are temporary: the default expiration is one hour and the maximum expiration is seven days.

I'm going to rant here that the getSignedUrl documentation never says that your signed URL will expire in a week. The documentation code has 3-17-2025 as the expiration date, suggesting that you can set the expiration years in the future. My app worked perfectly, and then crashed a week later. The error message said that the signatures didn't match, not that the download URL had expired. I made various changes to my code, and everything worked...until it all crashed a week later. This went on for more than a month of frustration. Is the 3-17-2025 date an inside joke? Like the gold coins of a leprechaun that vanish when the leprechaun is out of sight, a St. Patrick's Day expiry date years in the future vanishes in two weeks, just when you thought your code was bug-free.

Public URL #1: Make Your File Publicly Available

You can set the permissions on your file to public read, as explained in the documentation. This can be done from the Cloud Storage Browser or from your Node server. You can make one file public or a directory or your entire Storage database. Here's the Node code:

var webmPromise = new Promise(function(resolve, reject) {
      var options = {
        destination: ('Audio/' + longLanguage + '/' + pronunciation + '/' + word + '.mp3'),
        predefinedAcl: 'publicRead',
        contentType: 'audio/' + audioType,
      };

      synthesizeParams.accept = 'audio/webm';
      var file = bucket.file('Audio/' + longLanguage + '/' + pronunciation + '/' + word + '.webm');
      textToSpeech.synthesize(synthesizeParams)
      .then(function(audio) {
        audio.pipe(file.createWriteStream(options));
      })
      .then(function() {
        console.log("webm audio file written.");
        resolve();
      })
      .catch(error => console.error(error));
    });

The result will look like this in your Cloud Storage Browser:

enter image description here

Anyone can then use the standard path to download your file:

https://storage.googleapis.com/languagetwo-cd94d.appspot.com/Audio/English/United_States-OED-0/system.mp3

Another way to make a file public is to use the method makePublic(). I haven't been able to get this to work, it's tricky to get the bucket and file paths right.

An interesting alternative is to use Access Control Lists. You can make a file available only to users whom you put on a list, or use authenticatedRead to make the file available to anyone who is logged in from a Google account. If there were an option "anyone who logged into my app using Firebase Auth" I would use this, as it would limit access to only my users.

Deprecated: Build Your Own Download URL with firebaseStorageDownloadTokens

Several answers describe an undocumented Google Storage object property firebaseStorageDownloadTokens. This was never an official Google Cloud Storage feature and it no longer works. Here's how it used to work.

You told Storage the token you wanted to use. You then generated a token with the uuid Node module. Four lines of code and you could build your own download URL, the same download URL you get from the console or getDownloadURL(). The four lines of code are:

const uuidv4 = require('uuid/v4');
const uuid = uuidv4();
metadata: { firebaseStorageDownloadTokens: uuid }
https://firebasestorage.googleapis.com/v0/b/" + bucket.name + "/o/" + encodeURIComponent('Audio/' + longLanguage + '/' + pronunciation + '/' + word + '.webm') + "?alt=media&token=" + uuid);

Here's the code in context:

var webmPromise = new Promise(function(resolve, reject) {
  var options = {
    destination: ('Audio/' + longLanguage + '/' + pronunciation + '/' + word + '.mp3'),
    contentType: 'audio/' + audioType,
    metadata: {
      metadata: {
        firebaseStorageDownloadTokens: uuid,
      }
    }
  };

      synthesizeParams.accept = 'audio/webm';
      var file = bucket.file('Audio/' + longLanguage + '/' + pronunciation + '/' + word + '.webm');
      textToSpeech.synthesize(synthesizeParams)
      .then(function(audio) {
        audio.pipe(file.createWriteStream(options));
      })
      .then(function() {
        resolve("https://firebasestorage.googleapis.com/v0/b/" + bucket.name + "/o/" + encodeURIComponent('Audio/' + longLanguage + '/' + pronunciation + '/' + word + '.webm') + "?alt=media&token=" + uuid);
      })
      .catch(error => console.error(error));
});

That's not a typo--you have to nest firebaseStorageDownloadTokens in double layers of metadata:!

Thomas David Kehoe
  • 10,040
  • 14
  • 61
  • 100
  • 23
    I created an issue on `@google-cloud/storage` for this, feel free to +1 it ;) https://github.com/googleapis/nodejs-storage/issues/697 – Théo Champion May 15 '19 at 05:15
  • 2
    latest [makePublic()](https://googleapis.dev/nodejs/storage/latest/File.html#makePublic) link. – galki Nov 12 '19 at 10:13
  • 1
    It seems the `firebaseStorageDownloadTokens` does not work anymore. – Mason Nov 18 '19 at 07:20
  • 1
    The accepted answer suggests that it is not possible to get a persistent download url that does not expire which is not correct. The detail here in your answer is excellent and should be marked as the correct answer. Thank you. – DevMike Nov 20 '19 at 04:28
  • getDownloadURL() returns the url which is accessible by anyone and it doesn't require user to be authenticated, so it is less secure. – Nainal Dec 05 '19 at 12:02
  • 2
    @thomas thanks for the awesome summary! You mentioned there are 3 ways to get a persistent token download URL but you shared only 2: (a)From the Firebase Storage Console, and (b)getDownloadURL() From the Front End. I wonder what is the 3rd way? – czphilip Feb 07 '20 at 07:00
  • 2
    @czphilip If you want to just use a public Url you can do `await admin.storage().bucket(object.bucket).file(object.name!).makePublic();` then `object.mediaLink` to make that one file public – E. Sun Apr 11 '20 at 21:26
  • 1
    That's a very very useful article. I spend some time investigating and gathering information from Google and you kind of saved me a bunch of hours. – atereshkov Dec 11 '20 at 11:38
  • 2
    what is the benefit of getDownloadUrl vs. a public object? Can't every access each link? Is the only benefit that you can revoke access? – Andrew Jul 21 '21 at 06:50
  • 1
    Does anyone have insight into Andrew's question? I am wondering the same thing: for a public file, why not just access the generic public URL (i.e. storage.googleapis.com/bucketName/fileName)? – Redneys Nov 17 '21 at 23:15
  • 1
    **firebaseStorageDownloadTokens** still works. Change the metadata tested on May 26, 2023. The method used to save the file and get the url was `file.save(file, {metadata: {metadata: {firebaseStorageDownloadTokens: uuid}}})` – Aliton Oliveira May 27 '23 at 00:59
  • 1
    What do you mean by "You could upload files from the front end, but this would expose your credentials to anyone who downloads your app." ? I would assume that Firebase has at least basic security mechanisms in place that prevent unallowed access, no? – rsp1984 Jul 01 '23 at 19:40
  • Yeah, @rsp1984, I was wondering the same thing -- hope there is some insight – jdost_26 Jul 08 '23 at 13:01
  • 1
    @ThomasDavidKhoe: A new `getDownloadURL` function was added to version 11.10 of the Firebase Admin SDK for Node.js. See the new documentation on [creating a shareable URL](https://firebase.google.com/docs/storage/admin/start#shareable_urls) or [my answer](https://stackoverflow.com/a/76744881/209103) below (for now). – Frank van Puffelen Jul 22 '23 at 16:47
117

Here's an example on how to specify the download token on upload:

const UUID = require("uuid-v4");

const fbId = "<YOUR APP ID>";
const fbKeyFile = "./YOUR_AUTH_FIlE.json";
const gcs = require('@google-cloud/storage')({keyFilename: fbKeyFile});
const bucket = gcs.bucket(`${fbId}.appspot.com`);

var upload = (localFile, remoteFile) => {

  let uuid = UUID();

  return bucket.upload(localFile, {
        destination: remoteFile,
        uploadType: "media",
        metadata: {
          contentType: 'image/png',
          metadata: {
            firebaseStorageDownloadTokens: uuid
          }
        }
      })
      .then((data) => {

          let file = data[0];

          return Promise.resolve("https://firebasestorage.googleapis.com/v0/b/" + bucket.name + "/o/" + encodeURIComponent(file.name) + "?alt=media&token=" + uuid);
      });
}

then call with

upload(localPath, remotePath).then( downloadURL => {
    console.log(downloadURL);
  });

The key thing here is that there is a metadata object nested within the metadata option property. Setting firebaseStorageDownloadTokens to a uuid-v4 value will tell Cloud Storage to use that as its public auth token.

Many thanks to @martemorfosis

Panagiotis Panagi
  • 9,927
  • 7
  • 55
  • 103
Drew Beaupre
  • 2,437
  • 1
  • 17
  • 17
  • How do I get a valid UUID token for a file that is already uploaded on Storage? Generating random UUID did not help. Any pointers? – DerFaizio Jun 16 '17 at 08:14
  • 3
    Found the answer in @martemorfosis post. The UUID can be retrieved from the object.metadata exports.uploadProfilePic = functions.storage.object().onChange(event => { const object = event.data; // The Storage object. const uuid = object.metadata.firebaseStorageDownloadTokens; // ... – DerFaizio Jun 16 '17 at 10:02
  • Thank you for the bucket example! I was trying different combinations for the bucket and file methods for almost 1 hour :) – JCarlosR Aug 29 '17 at 18:34
  • 1
    Thanks for your answer! In my case, I was uploading with bucket.file(fileName).createWriteStream which doesn't return data when finishes upload, as a result, I used encodeURIComponent(fileName) instead of encodeURIComponent(file.name). – Stanislau Buzunko Mar 26 '18 at 08:55
  • Awesome! I used the same technique with PHP GCS client library! – Michel Apr 10 '19 at 08:32
  • Can you tell me if the resulting URL is equivalent to a *downloadUrl* that is retrievable via the client iOS or Android SDKs? It seems to me that the downloadURL will not expire whereas a signedUrl via the admin SDK does. Id like to retrieve a url that will not expire. – DevMike Nov 20 '19 at 04:21
  • 2
    This should be the right answer. It results in a URL similar to the one generated by the Firebase SDKs(@DevMike), and I bet its exactly what they do behind the scenes. – Samuel E. Nov 20 '19 at 11:59
  • 1
    I tried this earlier today and while the upload itself was successful, composing a link in the way you outlined did not work for me, any ideas? I ran into permissions issues even for authenticated clients. – DevMike Nov 20 '19 at 14:07
  • what is the point of doing this vs. just having it be public? Anyone can use the same link – Andrew Jul 21 '21 at 06:47
  • I agree that this is the correct answer. Signed URLs expire, which is a deal breaker. The 'mediaLink' URLs don't always work correctly (no cache headers, 206 response on byte range requests, etc). This method appears to perfectly mimic the client storage getDownloadURL method, which is exactly what I wanted. – Pete Shilling Nov 17 '21 at 20:11
  • For a public file, is there any reason to use this approach instead of just making the Cloud Storage bucket public and accessing the generic public URL (i.e. storage.googleapis.com/bucketName/fileName)? – Redneys Nov 17 '21 at 23:06
39

If you're working on a Firebase project, you can create signed URLs in a Cloud Function without including other libraries or downloading a credentials file. You just need to enable the IAM API and add a role to your existing service account (see below).

Initialize the admin library and get a file reference as your normally would:

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

admin.initializeApp(functions.config().firebase)

const myFile = admin.storage().bucket().file('path/to/my/file')

You then generate a signed URL with

myFile.getSignedUrl({action: 'read', expires: someDateObj}).then(urls => {
    const signedUrl = urls[0]
})

Make sure your Firebase service account has sufficient permissions to run this

  1. Go to the Google API console and enable the IAM API (https://console.developers.google.com/apis/api/iam.googleapis.com/overview)
  2. Still in the API console, go to the main menu, "IAM & admin" -> "IAM"
  3. Click edit for the "App Engine default service account" role
  4. Click "Add another role", and add the one called "Service Account Token Creator"
  5. Save and wait a minute for the changes to propagate

With a vanilla Firebase config, the first time you run the above code you'll get an error Identity and Access Management (IAM) API has not been used in project XXXXXX before or it is disabled.. If you follow the link in the error message and enable the IAM API, you'll get another error: Permission iam.serviceAccounts.signBlob is required to perform this operation on service account my-service-account. Adding the Token Creator role fixes this second permission issue.

SMX
  • 1,372
  • 15
  • 14
  • I was just about to leave an answer with basically these same details that I FINALLY figured out the hard way - sure wish I had read through the solutions this far down earlier :/ This worked for me as of 12/12/18. Thanks for the detailed instructions, very helpful for us beginners!! – Kat Dec 12 '18 at 15:29
  • 3
    My signedurl is expiring in 2 weeks but I am using admin.initializeApp() without key, is this the problem ? I had App Engine app default service account set to "owner" and Cloud Functions Service Agent, I just removed "owner" for now and added "Service Account Token Creator" – Amit Bravo Feb 14 '19 at 05:37
  • 2
    Signed URLs expire in 7 days. You can set a shorter expiration date but not longer. – Thomas David Kehoe Apr 25 '19 at 22:19
  • How to refresh url if it expires? – Manoj MM Jul 09 '19 at 05:20
  • how to refresh url to set it to longer time ? – Saifallak Jul 20 '19 at 16:02
  • 4
    I'm getting the error "Cannot sign data without `client_email`." using the emulator – Alynva Jul 09 '20 at 03:55
  • This works for me using google-cloud/storage `const { Storage } = require("@google-cloud/storage"); const gcs = new Storage({ projectId: projectId, keyFilename: './xxxx.json' });` https://stackoverflow.com/questions/39848132/upload-files-to-firebase-storage-using-node-js – Harpreet Singh Jan 16 '23 at 11:41
  • I am having this issue that that when trying to curl (Put) using the generated signedUrl to upload "MalformedSecurityHeader" "Header was included in signedheaders, but not in the request.", how do you use the generated URL in your clients? I tried using the example from the docs https://cloud.google.com/storage/docs/access-control/signing-urls-with-helpers#upload-object ``` "curl -X PUT -H 'Content-Type: application/octet-stream' " + `--upload-file my-file '${url}'` ``` – MorenoMdz Jul 04 '23 at 13:12
31

You should avoid harcoding URL prefix in your code, especially when there are alternatives. I suggest using the option predefinedAcl: 'publicRead' when uploading a file with Cloud Storage NodeJS 1.6.x or +:

const options = {
    destination: yourFileDestination,
    predefinedAcl: 'publicRead'
};

bucket.upload(attachment, options);

Then, getting the public URL is as simple as:

bucket.upload(attachment, options).then(result => {
    const file = result[0];
    return file.getMetadata();
}).then(results => {
    const metadata = results[0];
    console.log('metadata=', metadata.mediaLink);
}).catch(error => {
    console.error(error);
});
Laurent
  • 14,122
  • 13
  • 57
  • 89
  • file.getMetadata() did the trick for me after using the save() method on the file reference. Using it in NodeJS with firebase-admin sdk. – turbopasi Dec 16 '18 at 23:34
27

This is what I currently use, it's simple and it works flawlessly.

You don't need to do anything with Google Cloud. It works out of the box with Firebase..

// Save the base64 to storage.
const file = admin.storage().bucket('url found on the storage part of firebase').file(`profile_photos/${uid}`);
await file.save(base64Image, {
    metadata: {
      contentType: 'image/jpeg',
    },
    predefinedAcl: 'publicRead'
});
const metaData = await file.getMetadata()
const url = metaData[0].mediaLink

EDIT: Same example, but with upload:

await bucket.upload(fromFilePath, {destination: toFilePath});
file = bucket.file(toFilePath);
metaData = await file.getMetadata()
const trimUrl = metaData[0].mediaLink

#update: no need to make two different call in upload method to get the metadata:

let file = await bucket.upload(fromFilePath, {destination: toFilePath});
const trimUrl = file[0].metaData.mediaLink
Debug Diva
  • 26,058
  • 13
  • 70
  • 123
Oliver Dixon
  • 7,012
  • 5
  • 61
  • 95
25

With the recent changes in the functions object response you can get everything you need to "stitch" together the download URL like so:

 const img_url = 'https://firebasestorage.googleapis.com/v0/b/[YOUR BUCKET]/o/'
+ encodeURIComponent(object.name)
+ '?alt=media&token='
+ object.metadata.firebaseStorageDownloadTokens;

console.log('URL',img_url);
Demian S
  • 299
  • 4
  • 7
  • 2
    Are you referring to the object response from `bucket.file().upload()`? I don't receive any metadata property in the response data, and I'm not sure how to get these `firebaseStorageDownloadTokens`. – Dygerati Jul 11 '18 at 20:30
  • also [YOUR BUCKET] is `bucket.name`, you don't have to hardcode it or use an extra local var – Călin Darie Jul 13 '18 at 07:28
  • 4
    The problem with this solution is that the service URL is hardcoded. If the Firebase/Google change it, it may break. Using the `metadata.mediaLink` property prevents such an issue. – Laurent Aug 02 '18 at 13:39
  • 3
    It's not supported case to build a URL like this. It may work today, but could break in the future. You should only use the provided APIs to generate a proper download URL. – Doug Stevenson Nov 12 '18 at 06:09
  • 1
    Relying on a hardcoded URL that may change is a bad choice. – Laurent Nov 16 '18 at 20:01
  • agree, but the idea here is to show how to get the image url. You can put the url in a config or env variable etc. – Demian S Nov 17 '18 at 03:14
  • 1
    While I also don't like the idea of persisting a hard coded URL, @DougStevenson (Google) suggested that a URL in the same format was designed to be persisted in his answer in https://stackoverflow.com/questions/53055190/what-is-the-purpose-behind-getdownloadurl-of-firebase-storage. It seems like all current URLs would have to continue to work for quite some time if people are persisting these, but that doesn't mean things won't change later. I've also found that there's a little more latency with the `firebasestorage` URLs over the super long signed ones. – jon_wu Apr 28 '19 at 02:20
  • this is a great answer that I've been looking for for a LONG time. Thanks for exposing the firebaseStorageDownloadTokens for me Demian S! – Everett Glovier Apr 08 '20 at 01:23
22

For those wondering where the Firebase Admin SDK serviceAccountKey.json file should go. Just place it in the functions folder and deploy as usual.

It still baffles me why we can't just get the download url from the metadata like we do in the Javascript SDK. Generating a url that will eventually expire and saving it in the database is not desirable.

Clinton
  • 973
  • 6
  • 14
19

One method I'm using with success is to set a UUID v4 value to a key named firebaseStorageDownloadTokens in the metadata of the file after it finishes uploading and then assemble the download URL myself following the structure Firebase uses to generate these URLs, eg:

https://firebasestorage.googleapis.com/v0/b/[BUCKET_NAME]/o/[FILE_PATH]?alt=media&token=[THE_TOKEN_YOU_CREATED]

I don't know how much "safe" is to use this method (given that Firebase could change how it generates the download URLs in the future ) but it is easy to implement.

11

Sorry but i can't post a comment to your question above because of missing reputation, so I will include it in this answer.

Do as stated above by generating a signed Url, but instead of using the service-account.json I think you have to use the serviceAccountKey.json which you can generate at (replace YOURPROJECTID accordingly)

https://console.firebase.google.com/project/YOURPROJECTID/settings/serviceaccounts/adminsdk

Example:

const gcs = require('@google-cloud/storage')({keyFilename: 'serviceAccountKey.json'});
// ...
const bucket = gcs.bucket(bucket);
// ...
return bucket.upload(tempLocalFile, {
        destination: filePath,
        metadata: {
          contentType: 'image/jpeg'
        }
      })
      .then((data) => {
        let file = data[0]
        file.getSignedUrl({
          action: 'read',
          expires: '03-17-2025'
        }, function(err, url) {
          if (err) {
            console.error(err);
            return;
          }

          // handle url 
        })
NiVeK92
  • 130
  • 6
10

I can't comment on the answer James Daniels gave, but I think this is very Important to read.

Giving out a signed URL Like he did seems for many cases pretty bad and possible Dangerous. According to the documentation of Firebase the signed url expires after some time, so adding that to your databse will lead to a empty url after a certain timeframe

It may be that misunderstood the Documentation there and the signed url doesn't expire, which would have some security issues as a result. The Key seems to be the same for every uploaded file. This means once you got the url of one file, someone could easily access files that he is not suposed to access, just by knowing their names.

If i missunderstood that then i would lvoe to be corrected. Else someone should probably Update the above named solution. If i may be wrong there

Renji
  • 310
  • 3
  • 13
8

Use file.publicUrl()

Async/Await

const bucket = storage.bucket('bucket-name');
const uploadResponse = await bucket.upload('image-name.jpg');
const downloadUrl = uploadResponse[0].publicUrl();

Callback

const bucket = storage.bucket('bucket-name');
bucket.upload('image-name.jpg', (err, file) => {
  if(!file) {
    throw err;
  }

  const downloadUrl = file.publicUrl();
})

The downloadUrl will be "https://storage.googleapis.com/bucket-name/image-name.jpg".

Please note that in order for the above code to work, you have to make the bucket or file public. To do so, follow the instructions here https://cloud.google.com/storage/docs/access-control/making-data-public. Also, I imported the @google-cloud/storage package directly not through the Firebase SDK.

goodonion
  • 1,401
  • 13
  • 25
6

If you use the predefined access control lists value of 'publicRead', you can upload the file and access it with a very simple url structure:

// Upload to GCS
const opts: UploadOptions = {
  gzip: true,
  destination: dest, // 'someFolder/image.jpg'
  predefinedAcl: 'publicRead',
  public: true
};
return bucket.upload(imagePath, opts);

You can then construct the url like so:

const storageRoot = 'https://storage.googleapis.com/';
const bucketName = 'myapp.appspot.com/'; // CHANGE TO YOUR BUCKET NAME
const downloadUrl = storageRoot + bucketName + encodeURIComponent(dest);
inorganik
  • 24,255
  • 17
  • 90
  • 114
5

firebaser here

Version 11.10 of the Admin SDK for Node.js adds a getDownloadURL() method. An example of how to use it can be found in the documentation on getting a shareable URL:

const { getStorage, getDownloadURL } = require('firebase-admin/storage');

const fileRef = getStorage().bucket("my-bucket").file("my-file");
const downloadURL= await getDownloadURL(fileRef);
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
3

I had the same issue, however, I was looking at the code of the firebase function example instead of the README. And Answers on this thread didn't help either...

You can avoid passing the config file by doing the following:

Go to your project's Cloud Console > IAM & admin > IAM, Find the App Engine default service account and add the Service Account Token Creator role to that member. This will allow your app to create signed public URLs to the images.

source: Automatically Generate Thumbnails function README

Your role for app engine should look like this:

Cloud Console

TheFullResolution
  • 1,251
  • 1
  • 15
  • 26
3

answer by https://stackoverflow.com/users/269447/laurent works best

const uploadOptions: UploadOptions = {
    public: true
};

const bucket = admin.storage().bucket();
[ffile] = await bucket.upload(oPath, uploadOptions);
ffile.metadata.mediaLink // this is what you need
  • If you don't care about security, THIS is the easier solution ! Thank you very much @Jasdeep I did const response = await upload.bucket(... ///// response[0]..metadata.mediaLink // this is what you need – Damien Romito Feb 04 '21 at 07:57
3

I saw this on the admin storage doc

const options = {
  version: 'v4',
  action: 'read',
  expires: Date.now() + 15 * 60 * 1000, // 15 minutes
};

// Get a v4 signed URL for reading the file
const [url] = await storage
  .bucket(bucketName)
  .file(filename)
  .getSignedUrl(options);

console.log('Generated GET signed URL:');
console.log(url);
console.log('You can use this URL with any user agent, for example:');
console.log(`curl '${url}'`);
Chukwuemeka Maduekwe
  • 6,687
  • 5
  • 44
  • 67
2

This works if you just need a public file with a simple URL. Note that this may overrule your Firebase storage rules.

bucket.upload(file, function(err, file) {
    if (!err) {
      //Make the file public
      file.acl.add({
      entity: 'allUsers',
      role: gcs.acl.READER_ROLE
      }, function(err, aclObject) {
          if (!err) {
              var URL = "https://storage.googleapis.com/[your bucket name]/" + file.id;
              console.log(URL);
          } else {
              console.log("Failed to set permissions: " + err);
          }
      });  
    } else {
        console.log("Upload failed: " + err);
    }
});
Dakine
  • 198
  • 7
2

Without signedURL() using makePublic()

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp()
var bucket = admin.storage().bucket();

// --- [Above] for admin related operations, [Below] for making a public url from a GCS uploaded object

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

exports.testDlUrl = functions.storage.object().onFinalize(async (objMetadata) => {
    console.log('bucket, file', objMetadata.bucket + ' ' + objMetadata.name.split('/').pop()); // assuming file is in folder
    return storage.bucket(objMetadata.bucket).file(objMetadata.name).makePublic().then(function (data) {
        return admin.firestore().collection('publicUrl').doc().set({ publicUrl: 'https://storage.googleapis.com/' + objMetadata.bucket + '/' + objMetadata.name }).then(writeResult => {
            return console.log('publicUrl', writeResult);
        });
    });
});
ersin-ertan
  • 2,283
  • 2
  • 18
  • 27
1

For those who are using Firebase SDK andadmin.initializeApp:

1 - Generate a Private Key and place in /functions folder.

2 - Configure your code as follows:

const serviceAccount = require('../../serviceAccountKey.json');
try { admin.initializeApp(Object.assign(functions.config().firebase, { credential: admin.credential.cert(serviceAccount) })); } catch (e) {}

Documentation

The try/catch is because I'm using a index.js that imports other files and creates one function to each file. If you're using a single index.js file with all functions, you should be ok with admin.initializeApp(Object.assign(functions.config().firebase, { credential: admin.credential.cert(serviceAccount) }));.

1

As of firebase 6.0.0 I was able to access the storage directly with the admin like this:

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

So I didn't need to add a service account. Then setting the UUID as referenced above worked for getting the firebase url.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
NickJ
  • 512
  • 4
  • 10
1

This is the best I came up. It is redundant, but the only reasonable solution that worked for me.

await bucket.upload(localFilePath, {destination: uploadPath, public: true});
const f = await bucket.file(uploadPath)
const meta = await f.getMetadata()
console.log(meta[0].mediaLink)
Tibor Udvari
  • 2,932
  • 3
  • 23
  • 39
0

I already post my ans... in below URL Where you can get full code with solution

How do I upload a base64 encoded image (string) directly to a Google Cloud Storage bucket using Node.js?

const uuidv4 = require('uuid/v4');
const uuid = uuidv4();

    const os = require('os')
    const path = require('path')
    const cors = require('cors')({ origin: true })
    const Busboy = require('busboy')
    const fs = require('fs')
    var admin = require("firebase-admin");


    var serviceAccount = {
        "type": "service_account",
        "project_id": "xxxxxx",
        "private_key_id": "xxxxxx",
        "private_key": "-----BEGIN PRIVATE KEY-----\jr5x+4AvctKLonBafg\nElTg3Cj7pAEbUfIO9I44zZ8=\n-----END PRIVATE KEY-----\n",
        "client_email": "xxxx@xxxx.iam.gserviceaccount.com",
        "client_id": "xxxxxxxx",
        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
        "token_uri": "https://oauth2.googleapis.com/token",
        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
        "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-5rmdm%40xxxxx.iam.gserviceaccount.com"
      }

    admin.initializeApp({
        credential: admin.credential.cert(serviceAccount),
        storageBucket: "xxxxx-xxxx" // use your storage bucket name
    });


    const app = express();
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());
app.post('/uploadFile', (req, response) => {
    response.set('Access-Control-Allow-Origin', '*');
    const busboy = new Busboy({ headers: req.headers })
    let uploadData = null
    busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
        const filepath = path.join(os.tmpdir(), filename)
        uploadData = { file: filepath, type: mimetype }
        console.log("-------------->>",filepath)
        file.pipe(fs.createWriteStream(filepath))
      })

      busboy.on('finish', () => {
        const bucket = admin.storage().bucket();
        bucket.upload(uploadData.file, {
            uploadType: 'media',
            metadata: {
              metadata: { firebaseStorageDownloadTokens: uuid,
                contentType: uploadData.type,
              },
            },
          })

          .catch(err => {
            res.status(500).json({
              error: err,
            })
          })
      })
      busboy.end(req.rawBody)
   });




exports.widgets = functions.https.onRequest(app);
Rawan-25
  • 1,753
  • 1
  • 17
  • 25
0

For those trying to use the token parameter to share the file and would like to use gsutil command, here is how I did it:

First you need to authenticate by running: gcloud auth

Then run:

gsutil setmeta -h "x-goog-meta-firebaseStorageDownloadTokens:$FILE_TOKEN" gs://$FIREBASE_REPO/$FILE_NAME

Then you can download the file with the following link:

https://firebasestorage.googleapis.com/v0/b/$FIREBASE_REPO/o/$FILE_NAME?alt=media&token=$FILE_TOKEN

quicklikerabbit
  • 3,257
  • 6
  • 26
  • 39
0

From the Admin SDKs, you cannot retrieve the download token generated by Firebase of an uploaded file, but you can set that token when uploading by adding it in the metadata.

For those who are working on Python SDK. This is the way to do it:

from firebase_admin import storage
from uuid import uuid4

bucket = storage.bucket()
blob = bucket.blob(path_to_file)
token = str(uuid4()) # Random ID

blob.metadata = {
        "firebaseStorageDownloadTokens": token
    }
blob.upload_from_file(file)

You have now uploaded the file and got the URL token. You could now save the token (or even the full download URL) into your database (e.g. Firestore) and send it to the client when the file is requested and then making the client itself retrieve the file.

The full download URL looks like this:

https://firebasestorage.googleapis.com/v0/b/{bucket_name}/o/{file_name}?alt=media&token={token}
0

tldr; Uploading a blob and getting the image Url:

const file = storage.bucket().file(`images/${imageName}.jpeg`)

await file.save(image)

const imgUrl = file.metadata.mediaLink
christianbauer1
  • 467
  • 5
  • 16
-1

If you are getting error:

Google Cloud Functions: require(…) is not a function

try this:

const {Storage} = require('@google-cloud/storage');
const storage = new Storage({keyFilename: 'service-account-key.json'});
const bucket = storage.bucket(object.bucket);
const file = bucket.file(filePath);
.....
Wild Goat
  • 3,509
  • 12
  • 46
  • 87