Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storage): Add getDownloadUrl method to the Storage API #2036

Merged
merged 34 commits into from
Jun 7, 2023

Conversation

maneesht
Copy link
Contributor

@maneesht maneesht commented Jan 6, 2023

Fixes #1352

@maneesht maneesht requested a review from tonyjhuang January 6, 2023 21:53
if (!metadata?.metadata?.firebaseStorageDownloadTokens) {
throw new FirebaseError({
code: 'storage/no-download-token',
message: 'No download token available. Please create one in the Firebase Console.',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we actually do have a token API that the console uses. We would have to implement it here as well but perhaps we could point users to that method instead of the console

@lahirumaramba lahirumaramba self-assigned this Jan 9, 2023
@lahirumaramba lahirumaramba self-requested a review January 9, 2023 19:00
Copy link

@lepatryk lepatryk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Maneesh for putting it together. I really think our customers would appreciate it.

However our download url design is a hot mess right now and I'm hoping we can clean it up in 2023. This means getDownloadUrl() in the current form will either be deprecated or at least discouraged.

Does it make sense to add it here if we are planning to deprecate it a few months later?

// Gets metadata from firebase backend instead of GCS
getFirebaseMetadata(): Promise<FirebaseMetadata> {
// We need to talk to the firebase storage endpoints instead of the google cloud bucket endpoint
const endpoint = "https://firebasestorage.googleapis.com/v0";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can configure this for testing against our internal testing endpoints or emulator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that you call getStorage(app, 'myendpoint') or getStorage(undefined, 'myEndpoint')

src/storage/cloud-extensions.ts Outdated Show resolved Hide resolved
src/storage/cloud-extensions.ts Outdated Show resolved Hide resolved
src/storage/cloud-extensions.ts Outdated Show resolved Hide resolved
src/storage/cloud-extensions.ts Outdated Show resolved Hide resolved
constructor(
bucket: FirebaseStorageBucket,
name: string,
private endpoint: string,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious how other SDKs handle env overrides. Could you confirm this is commonly how this is implemented?

Copy link
Contributor Author

@maneesht maneesht Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firestore just uses the google cloud package and does something like this:

 const settings: Settings = {
  host: "localhost:8080",
  ssl: false
};

// Initialize Firestore
const firestore = new Firestore(settings);

For RTDB, we ask them to override the databaseUrl:

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: "https://movie-picker-729bb-default-rtdb.firebaseio.com",
  });

So I don't think there's a best practices way to get this done. My recommendation is to do something like:

getStorage({ host: 'myhosturl' });

So that we can allow for more options to be customized down the road. WDYT?

EDIT: Introducing a new ENV var seems cleaner than piping through a bunch of classes. WDYT?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, I don't have a strong sense of what design is right here, env var sounds fine.

Eventually these patterns should be consolidated imo, but that can happen async.

* Gets metadata from firebase backend instead of GCS
* @returns {FirebaseMetadata}
*/
getFirebaseMetadata(): Promise<FirebaseMetadata> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be private?

});
}
const [token] = downloadTokens.split(",");
return `${this.endpoint}/v0/b/${this.bucket.name}/o/${encodeURIComponent(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like there's a bug here, getFirebaseMetadata above assumes this.endpoint has the /v0 url part.

this.endpoint = endpoint;
}
/**
*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want me to add more information about this?

super(bucket, name, options);
}
/**
* Gets metadata from firebase backend instead of GCS

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: Firebase

@@ -57,10 +60,12 @@ export class Storage {

process.env.STORAGE_EMULATOR_HOST = `http://${process.env.FIREBASE_STORAGE_EMULATOR_HOST}`;
}
this.endpoint = (userEndpoint || process.env.STORAGE_EMULATOR_HOST || 'https://firebasestorage.googleapis.com') + '/v0';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already have one for the emulator host, does it make sense to use another env var for the endpoint override?

Copy link

@tonyjhuang tonyjhuang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nits but overall lgtm

src/storage/cloud-extensions.ts Outdated Show resolved Hide resolved
uri,
},
(err, body) => {
console.log(body);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

* @returns {FirebaseMetadata}
*/
private getFirebaseMetadata(): Promise<FirebaseMetadata> {
// Build any custom headers based on the defined interceptors on the parent

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this comment still accurate?

}

/**
* Gets the download URL for a given file. Will throw a `FirebaseError` if there are no download tokens available.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/**
* Gets the download URL for a given file. Will throw a `FirebaseError` if there are no download tokens available.
* @returns {Promise<string>}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update with a description of the result or remove as this is redundant with the method signature

export class FirebaseStorageClient extends StorageClient {
/**
*
* @param bucketName

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update?

*/
export class FirebaseStorageBucket extends Bucket {
/**
* @param name

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update?

Copy link

@tonyjhuang tonyjhuang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall direction lgtm, small nits, can you go through the whole PR to make sure its up to date?

}
/**
* Gets metadata from Firebase backend instead of GCS
* @returns {FirebaseMetadata}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nit: jsdoc clauses unnecessary unless they add context that cant be inferred from the method signature

go/java-practices/javadoc#param


/**
* Gets the download URL for a given file. Will throw a `FirebaseError` if there are no download tokens available.
* @returns {Promise<string>}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment

endpoint: string,
file: File
): Promise<FirebaseMetadata> {
// Build any custom headers based on the defined interceptors on the parent

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this comment still up to date?

@@ -17,7 +19,8 @@ export function getStorage(app?: App): Storage;
// @public
export class Storage {
get app(): App;
bucket(name?: string): Bucket;
// Warning: (ae-forgotten-export) The symbol "FirebaseStorageBucket" needs to be exported by the entry point index.d.ts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update?

@@ -38,6 +37,12 @@ describe('admin.storage', () => {
return verifyBucket(bucket, 'storage().bucket(string)')
.should.eventually.be.fulfilled;
});
it('bucket(string) creates a download token', async () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update tests

@maneesht maneesht marked this pull request as ready for review May 8, 2023 15:22
@MorenoMdz
Copy link

MorenoMdz commented Jul 4, 2023

@tonyjhuang (or anyone else) Any idea when the next release will be?

Bumping this. cc @maneesht

Edit:

In the meantime, I got it to work by using the code from this PR directly in my service, thanks again @maneesht !!

utils:

export interface FirebaseMetadata {
  name: string
  bucket: string
  generation: string
  metageneration: string
  contentType: string
  timeCreated: string
  updated: string
  storageClass: string
  size: string
  md5Hash: string
  contentEncoding: string
  contentDisposition: string
  crc32c: string
  etag: string
  downloadTokens?: string
}

export function getFirebaseMetadata(
  endpoint: string,
  file: File,
): Promise<FirebaseMetadata> {
  const uri = `${endpoint}/b/${file.bucket.name}/o/${encodeURIComponent(
    file.name,
  )}`

  return new Promise((resolve, reject) => {
    file.storage.makeAuthenticatedRequest(
      {
        method: 'GET',
        uri,
      },
      (err, body) => {
        if (err) {
          reject(err)
        } else {
          resolve(body)
        }
      },
    )
  })
}

export async function getDownloadUrl(file: File): Promise<string> {
  const endpoint =
    (process.env.STORAGE_EMULATOR_HOST ||
      'https://firebasestorage.googleapis.com') + '/v0'
  const { downloadTokens } = await getFirebaseMetadata(endpoint, file)
  if (!downloadTokens) {
    throw new Error(
      'No download token available. Please create one in the Firebase Console.',
    )
  }
  const [token] = downloadTokens.split(',')
  return `${endpoint}/b/${file.bucket.name}/o/${encodeURIComponent(
    file.name,
  )}?alt=media&token=${token}`
}

service

  generateDownloadUrl(filePath: string) {
    const bucket = firebaseAdmin
      .storage()
      .bucket(this.configService.get('FIREBASE_STORAGE_BUCKET'))
    const file = bucket.file(filePath)

    return getDownloadUrl(file)
  }

@tonyjhuang
Copy link

Hi, we expect this change to rollout this week. You'll be able to follow along with the release at https://firebase.google.com/support/release-notes/admin/node

lahirumaramba added a commit that referenced this pull request Jul 12, 2023
lahirumaramba added a commit that referenced this pull request Jul 12, 2023
@tonyjhuang
Copy link

This feature is now available in https://github.com/firebase/firebase-admin-node/releases/tag/v11.10.0 🥳

@okcoker
Copy link

okcoker commented Jul 18, 2023

Congrats and thanks, @maneesht!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release:stage Stage a release candidate release-note
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FR] Admin 'getDownloadURL' for Storage
10 participants