使用Firebase Cloud Functions上传文件后,获取下载URL。

199
在使用Firebase的Functions for Firebase上传文件后,我想要获取文件的下载链接。
我有以下代码:
...

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

        // Get the download url of file

    });

对象文件有很多参数,甚至包括一个名为mediaLink的参数。但是,如果我尝试访问这个链接,我会收到以下错误消息:

匿名用户没有访问对象...的storage.objects.get权限

有人可以告诉我如何获取公共下载链接吗?
谢谢

请参见此帖子,该帖子在函数可用的数据中重建了URL。 - Kato
只有在您没有Firebase安全规则,即允许在所有条件下进行读写时,此模式才足够:"https://firebasestorage.googleapis.com/v0/b/<project-id>.appspot.com/o/<file name>?alt=media"。 - Inzamam Malik
如果auth为null,则只有在没有读写权限时才需要签名URL或令牌。 - Inzamam Malik
27个回答

187
你需要使用getSignedURL通过@google-cloud/storage NPM模块生成一个带有签名的URL。
示例:
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
});

你需要使用你的服务帐号凭据来初始化@google-cloud/storage,因为应用程序默认凭据是不够的。 更新:现在可以通过Firebase Admin SDK访问Cloud Storage SDK,它作为@google-cloud/storage的包装器。唯一的方法是:
  1. 使用特殊的服务帐号初始化SDK,通常通过第二个非默认实例。
  2. 或者,如果没有服务帐号,给默认的App Engine服务帐号授予"signBlob"权限。
更新(2023年7月):在Firebase Admin SDK for Node.js的11.10版本中添加了一个新的getDownloadURL函数。请参阅关于创建可共享URLpuf's answer的新文档。

110
这很奇怪。当使用Firebase Android、iOS和Web SDK时,我们可以轻松地从存储引用中获取下载URL。但在管理员权限下为什么没有那么容易呢?PS: 我应该在哪里找到初始化gcs所需的“service-account.json”文件? - Valentin
3
这是因为 admin-sdk 没有任何云存储的附加功能。您可以在此处获取管理员 SDK 服务帐号 JSON:https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk - James Daniels
25
用这种方法生成的URL太长了。@martemorfosis提议的方法生成的URL要好得多。有没有能够生成那种URL的函数?那是我在使用firebase-sdk时保存在数据库中以供将来使用的内容。需要在Functions领域中存在一种镜像方法。 - Bogac
3
我们应该如何将service-account.json文件与部署的函数一起上传?我试过将它放在functions文件夹中,并在package.json的file字段中引用它,但它并没有被部署。谢谢。 - David Aroesti
13
注意!已签名的 URL 不适合长时间使用,最多只能在 2 周内过期(v4)。如果您打算将其长期存储在数据库中,已签名的 URL 并不是正确的方法。请检查此处的属性“expires”:https://googleapis.dev/nodejs/storage/latest/global.html#GetBucketSignedUrlConfig。 - maganap
显示剩余16条评论

145
本答案将总结上传文件到Google / Firebase Cloud Storage时获取下载URL的选项。有三种类型的下载URLS:
1.持久且具有安全功能的{{令牌}}下载URLS 2.临时且具有安全功能的{{已签名}}下载URLS 3.持久且缺乏安全性的{{公共}}下载URLS
有两种方法可以获得{{令牌}}下载URL。{{已签名}}和{{公共}}下载URL各有一种获取方式。
{{令牌}} URL方法#1:从Firebase Storage控制台
您可以从Firebase Storage控制台获取下载URL:

enter image description here

下载链接看起来像这样:

{{下载URL}}

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

第一部分是指向您文件的标准路径。末尾是令牌。这个下载URL是永久的,即它不会过期,尽管您可以撤销它。

令牌URL方法#2:从前端

文档告诉我们要使用getDownloadURL()

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

这将获取与您从Firebase Storage控制台获取的相同的下载URL。这种方法很容易,但需要您知道文件的路径,在我的应用程序中这很困难。您可以从前端上传文件,但这会使下载您的应用程序的任何人都能看到您的凭据。因此,对于大多数项目,您将希望从Cloud Functions上传文件,然后获取下载URL并将其保存到数据库中,以及有关文件的其他数据。
当我从Cloud Function写入文件到Storage时(因为我找不到一种方法来告诉前端文件已写入Storage),我找不到获取令牌下载URL的方法,但对我有效的方法是将文件写入公开可用的URL,将公开可用的URL写入Firebase,然后当我的Angular前端从Firebase获取下载URL时,它也运行getDownloadURL()(其中包含令牌),然后将Firestore中的下载URL与令牌下载URL进行比较,如果它们不匹配,则在Firestore中将令牌下载URL更新为公开可用URL。这只会将您的文件公开一次。
这比听起来要容易得多。以下代码遍历存储下载URL数组,并使用令牌下载URL替换公开可用的下载URL。
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

你可能会想,“我将从我的云函数返回存储位置到前端,然后使用getDownloadURL()与Firestore一起写入令牌下载URL。”这样做不起作用,因为云函数只能返回同步结果。异步操作会返回null
你可能会说:“没问题,我将在Storage上设置一个Observer,从Observer获取位置,然后使用getDownloadURL()将位置与Firestore一起写入令牌下载URL。” 不行,Firestore有观察者,但Storage没有。
你可能会说:“那我从前端调用listAll(),获取所有Storage文件的列表,然后调用每个文件的metadata,并提取每个文件的下载URL和令牌,然后将它们写入Firestore?”很好的尝试,但是Storage元数据不包括下载URL或令牌。 签名URL方法#1:用于临时下载URL的getSignedUrl() 从云函数中使用getSignedUrl()很容易:
  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);
            }
          });
        });
      });
    });
  }

签署的下载URL如下所示:

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

已签名的URL具有过期日期和长签名。命令行gsutil signurl -d的文档说明签名URL是临时的:默认过期时间为一小时,最大过期时间为七天。
我要在这里发牢骚,getSignedUrl文档从未说过您的已签名URL将在一周内过期。文档代码将3-17-2025作为到期日期,表明您可以将有效期设置为未来几年。我的应用程序完美地运行了一段时间,然后在一周后崩溃了。错误消息说签名不匹配,而不是下载URL已过期。我对我的代码进行了各种更改,一切都正常...直到一周后又崩溃了。这种情况持续了一个多月的挫折。 3-17-2025日期是否是内部玩笑?就像在视线中看不到小矮人时消失的金币一样,未来数年的圣帕特里克节到期日在两周后消失,就在您认为您的代码没有漏洞时。
公共URL#1:使您的文件公开可用。

您可以按照文档中的说明将文件权限设置为公共读取。这可以通过Cloud Storage浏览器或Node服务器完成。您可以使一个文件公开,也可以使目录或整个存储数据库公开。以下是Node代码:

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

你在云存储浏览器中看到的结果将是这样的:

enter image description here

任何人都可以使用标准路径下载您的文件:

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

另一种使文件公开的方法是使用makePublic()方法。我无法让它起作用,很难正确获取存储桶和文件路径。

一个有趣的替代方法是使用访问控制列表。您可以仅将文件提供给您列出的用户,或使用authenticatedRead使文件对从Google帐户登录的任何人都可用。如果有一个“任何使用Firebase Auth登录我的应用程序的人”选项,我会使用它,因为它只限制对我的用户的访问。

已弃用:使用firebaseStorageDownloadTokens构建自己的下载URL

几个答案描述了一个未记录在Google Storage对象属性中的firebaseStorageDownloadTokens。这从未是官方的Google Cloud Storage功能,现在已不再起作用。以下是它的工作方式。

您告诉Storage要使用的令牌。然后,您使用uuid Node模块生成令牌。四行代码,您就可以构建自己的下载URL,与控制台或getDownloadURL()获得的相同的下载URL。这四行代码是:

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

以下是上下文中的代码:

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

这不是打错了--你需要将firebaseStorageDownloadTokens嵌套在metadata:的双层中!


23
我在 @google-cloud/storage 上创建了一个问题,欢迎给它点赞 ;) https://github.com/googleapis/nodejs-storage/issues/697 - Théo Champion
2
最新的 makePublic() 链接。 - galki
2
@thomas,感谢您提供的精彩总结!您提到了获取持久令牌下载URL的三种方法,但是您只分享了其中两种:(a)从Firebase存储控制台获取,以及(b)从前端获取getDownloadURL()。我想知道第三种方法是什么? - czphilip
2
如果您想使用公共URL,可以执行以下操作:await admin.storage().bucket(object.bucket).file(object.name!).makePublic(); 然后使用 object.mediaLink 将该文件设为公共。 - E. Sun
2
getDownloadUrl相对于公共对象的好处是什么?每个人都不能访问每个链接吗?唯一的好处是可以撤销访问权限吗? - Andrew
显示剩余9条评论

117

这里是一个关于如何在上传时指定下载令牌的示例:

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

然后用以下方式进行调用:

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

关键在于metadata选项属性内嵌了一个metadata对象。将firebaseStorageDownloadTokens设置为uuid-v4值将告诉Cloud Storage使用它作为其公共授权令牌。

非常感谢@martemorfosis


3
在@martemorfosis的帖子中找到了答案。UUID可以从object.metadata中检索到。exports.uploadProfilePic = functions.storage.object().onChange(event => { const object = event.data; // Storage对象。 const uuid = object.metadata.firebaseStorageDownloadTokens; // ... - DerFaizio
1
谢谢您的回答! 在我的情况下,我是使用bucket.file(fileName).createWriteStream上传文件,它在上传完成时不返回数据,因此我使用encodeURIComponent(fileName)代替encodeURIComponent(file.name)。 - Stanislau Buzunko
2
这应该是正确的答案。它会生成一个类似于Firebase SDKs生成的URL,我敢打赌这正是他们在幕后所做的。 - Samuel E.
1
我今天早些时候尝试了这个,虽然上传本身成功了,但按照你概述的方式组合链接对我没有起作用,有什么想法吗?即使对于经过身份验证的客户端,我也遇到了权限问题。 - DevMike
对于公共文件,使用这种方法是否有任何理由,而不是使Cloud Storage存储桶公开,并访问通用的公共URL(即storage.googleapis.com/bucketName/fileName)? - Redneys
显示剩余6条评论

39

如果您正在使用Firebase项目,您可以在Cloud Function中创建签名URL而无需包含其他库或下载凭据文件。您只需要启用IAM API并向现有服务帐户添加角色(请参见下文)。

像平常一样初始化admin库并获取文件引用:

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

然后您可以使用签名 URL 生成...

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

确保您的Firebase服务帐户具有足够的权限来运行此程序

  1. 前往Google API控制台并启用IAM API (https://console.developers.google.com/apis/api/iam.googleapis.com/overview)
  2. 仍在API控制台中,进入主菜单,"IAM和管理"->"IAM"
  3. 点击"App Engine默认服务帐户"角色的编辑按钮
  4. 点击"添加其他角色",并添加名为"Service Account Token Creator"的角色
  5. 保存并等待一分钟以使更改生效

如果使用原始的Firebase配置,在第一次运行上述代码时,您将收到一个错误消息:Identity and Access Management (IAM) API has not been used in project XXXXXX before or it is disabled. 如果您按照错误消息中的链接启用IAM API,则会收到另一个错误消息:Permission iam.serviceAccounts.signBlob is required to perform this operation on service account my-service-account。 添加“Token Creator”角色可以解决这个第二个权限问题。


3
我的signedurl还有两周就过期了,但我正在使用admin.initializeApp()而没有密钥,这是问题吗?我的App Engine应用程序默认服务帐户设置为“owner”和Cloud Functions服务代理,我暂时删除了“owner”,并添加了“Service Account Token Creator”。 - Amit Bravo
2
签名 URL 在 7 天后过期。您可以设置更短的过期时间,但不能设置更长的时间。 - Thomas David Kehoe
如果URL过期了,如何刷新它? - Manoj MM
如何刷新URL以将其设置为较长的时间? - Saifallak
4
我遇到了“无法在模拟器中签署数据,缺少'client_email'”的错误提示。 - Alynva
显示剩余3条评论

31

您应避免在代码中硬编码URL前缀,特别是当有替代方案时。 我建议在使用Cloud Storage NodeJS 1.6.x或更高版本上传文件时使用选项predefinedAcl:'publicRead'

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

bucket.upload(attachment, options);

获取公共URL就像这样简单:

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

在使用文件引用的save()方法后,file.getMetadata()对我很有帮助。在NodeJS中使用firebase-admin sdk。 - Pascal Lamers

27

这是我目前使用的东西,它简单易用,而且功能完美。

您不需要在Google Cloud上做任何事情。它可以直接与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

编辑: 同样的例子,但加上上传:

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

#更新: 在上传方法中不需要进行两次不同的调用以获取元数据:

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

1
你如何在文件不是base64编码的情况下使用它? - Tibor Udvari
2
它不是mediaLinkenter,只是mediaLink。 - l2aelba
1
我找不到 mediaLink https://i.stack.imgur.com/B4Fw5.png - sarah
2
@OliverDixon 这个方法有时间限制吗?我的意思是,接受的答案中的signedURL仅在7天内有效。那么使用此mediaLink生成的URL呢?超过7天了吗? - Alexa289
我认为 metatata 应该是 metaData - charles-allen
显示剩余2条评论

25

最近对于对象响应函数的修改,使得您可以获取一切需要的内容来"拼接"下载URL,如下所示:

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

2
你是指来自 bucket.file().upload() 的对象响应吗?我在响应数据中没有收到任何元数据属性,也不确定如何获取这些 firebaseStorageDownloadTokens - Dygerati
4
这个解决方案的问题在于服务URL是硬编码的。如果Firebase/Google更改了它,那么可能会出现问题。使用 metadata.mediaLink 属性可以避免这种问题。 - Laurent
3
不支持使用此方式构建URL。虽然现在可能可用,但未来可能会失效。您应该仅使用提供的API生成正确的下载链接。 - Doug Stevenson
1
依赖于一个可能会改变的硬编码URL是一个不好的选择。 - Laurent
1
虽然我也不喜欢将硬编码的URL持久化的想法,但@DougStevenson(Google)在他在https://dev59.com/Qa_la4cB1Zd3GeqPtHNP中的回答中建议使用相同格式的URL进行持久化。如果人们持久化这些URL,似乎所有当前的URL都必须继续工作相当长的时间,但这并不意味着事情以后不会改变。我还发现`firebasestorage` URL比超长签名的URL具有更多的延迟。 - jon_wu
显示剩余3条评论

22

对于那些想知道 Firebase Admin SDK 的 serviceAccountKey.json 文件应该放在哪里的人。只需将其放置在 functions 文件夹中,然后像往常一样部署即可。

仍然让我感到困惑的是,为什么我们不能像在 Javascript SDK 中那样从元数据中获取下载 URL。生成最终会过期并保存在数据库中的 URL 并不理想。


19

我目前使用的一种成功的方法是,在文件上传完成后,将UUID v4值设置为一个名为firebaseStorageDownloadTokens的密钥的元数据中,然后按照Firebase用于生成这些URL的结构自己组装下载URL,例如:

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

我不知道使用这种方法有多“安全”(因为Firebase未来可能会更改生成下载URL的方式),但它很容易实现。


1
你有设置uuid值的示例吗? - Drew Beaupre
1
我和Drew有同样的问题,你在哪里设置元数据?我尝试在bucket.upload函数中设置,但没有起作用。 - Vysakh Sreenivasan
1
Vysakh,我已经发布了一个带有示例的完整答案。希望能对你有所帮助。 - Drew Beaupre
你在哪里/如何创建令牌? - CodyBugstein
5
我认为这种技术并不“安全”,因为下载链接应该是不透明的,它的组成部分不能被拆解或组合。 - Doug Stevenson

11

很抱歉因为声望不足,我无法在您上面的问题中发表评论,因此我会将它包含在这个回答中。

按照上述说明生成已签名的URL,但是不要使用service-account.json,而是使用你可以在以下位置生成的serviceAccountKey.json(请替换YOURPROJECTID):

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

示例:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接