Get Download URL from file uploaded with Cloud Fun

2019-01-01 04:22发布

问题:

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

回答1:

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.


回答2:

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



回答3:

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);


回答4:

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.



回答5:

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.



回答6:

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);
});


回答7:

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 
        })


回答8:

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.



回答9:

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);
    }
});


回答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



回答11:

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) }));.



回答12:

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



回答13:

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.