Skip to content

Commit

Permalink
Merge branch 'master' into totp-user-record
Browse files Browse the repository at this point in the history
  • Loading branch information
pragatimodi committed Jun 12, 2023
2 parents c07396e + 625e8d7 commit 1339d59
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 42 deletions.
4 changes: 4 additions & 0 deletions etc/firebase-admin.storage.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

import { Agent } from 'http';
import { Bucket } from '@google-cloud/storage';
import { File } from '@google-cloud/storage';

// @public
export function getDownloadUrl(file: File): Promise<string>;

// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts
//
Expand Down
68 changes: 34 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions src/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
* @packageDocumentation
*/

import { File } from '@google-cloud/storage';
import { App, getApp } from '../app';
import { FirebaseApp } from '../app/firebase-app';
import { Storage } from './storage';
import { FirebaseError } from '../utils/error';
import { getFirebaseMetadata } from './utils';

export { Storage } from './storage';


/**
* Gets the {@link Storage} service for the default app or a given app.
*
Expand Down Expand Up @@ -53,3 +57,34 @@ export function getStorage(app?: App): Storage {
const firebaseApp: FirebaseApp = app as FirebaseApp;
return firebaseApp.getOrInitService('storage', (app) => new Storage(app));
}



/**
* Gets the download URL for the given {@link @google-cloud/storage#File}.
*
* @example
* ```javascript
* // Get the downloadUrl for a given file ref
* const storage = getStorage();
* const myRef = ref(storage, 'images/mountains.jpg');
* const downloadUrl = await getDownloadUrl(myRef);
* ```
*/
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 FirebaseError({
code: 'storage/no-download-token',
message:
'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}`;
}
1 change: 0 additions & 1 deletion src/storage/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ export class Storage {
'explicitly when calling the getBucket() method.',
});
}

/**
* Optional app whose `Storage` service to
* return. If not provided, the default `Storage` service will be returned.
Expand Down
43 changes: 43 additions & 0 deletions src/storage/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { File } from '@google-cloud/storage';
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);
}
}
);
});
}
48 changes: 47 additions & 1 deletion test/integration/storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,23 @@ import * as chaiAsPromised from 'chai-as-promised';
import { Bucket, File } from '@google-cloud/storage';

import { projectId } from './setup';
import { getStorage } from '../../lib/storage/index';
import { getDownloadUrl, getStorage } from '../../lib/storage/index';
import { getFirebaseMetadata } from '../../src/storage/utils';
import { FirebaseError } from '../../src/utils/error';

chai.should();
chai.use(chaiAsPromised);

const expect = chai.expect;

describe('admin.storage', () => {
let currentRef: File | null = null;
afterEach(async () => {
if (currentRef) {
await currentRef.delete();
}
currentRef = null;
});
it('bucket() returns a handle to the default bucket', () => {
const bucket: Bucket = getStorage().bucket();
return verifyBucket(bucket, 'storage().bucket()')
Expand All @@ -39,13 +48,43 @@ describe('admin.storage', () => {
.should.eventually.be.fulfilled;
});

it('getDownloadUrl returns a download URL', async () => {
const bucket = getStorage().bucket(projectId + '.appspot.com');
currentRef = await verifyBucketDownloadUrl(bucket, 'testName');
// Note: For now, this generates a download token when needed, but in the future it may not.
const metadata = await getFirebaseMetadata(
'https://firebasestorage.googleapis.com/v0',
currentRef
);
if (!metadata.downloadTokens) {
expect(getDownloadUrl(currentRef)).to.eventually.throw(
new FirebaseError({
code: 'storage/invalid-argument',
message:
'Bucket name not specified or invalid. Specify a valid bucket name via the ' +
'storageBucket option when initializing the app, or specify the bucket name ' +
'explicitly when calling the getBucket() method.',
})
);
return;
}
const downloadUrl = await getDownloadUrl(currentRef);

const [token] = metadata.downloadTokens.split(',');
const storageEndpoint = `https://firebasestorage.googleapis.com/v0/b/${
bucket.name
}/o/${encodeURIComponent(currentRef.name)}?alt=media&token=${token}`;
expect(downloadUrl).to.equal(storageEndpoint);
});

it('bucket(non-existing) returns a handle which can be queried for existence', () => {
const bucket: Bucket = getStorage().bucket('non.existing');
return bucket.exists()
.then((data) => {
expect(data[0]).to.be.false;
});
});

});

function verifyBucket(bucket: Bucket, testName: string): Promise<void> {
Expand All @@ -66,3 +105,10 @@ function verifyBucket(bucket: Bucket, testName: string): Promise<void> {
expect(data[0], 'File not deleted').to.be.false;
});
}

async function verifyBucketDownloadUrl(bucket: Bucket, testName: string): Promise<File> {
const expected: string = 'Hello World: ' + testName;
const file: File = bucket.file('data_' + Date.now() + '.txt');
await file.save(expected)
return file;
}
4 changes: 2 additions & 2 deletions test/resources/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ export class MockComputeEngineCredential extends ComputeEngineCredential {
}
}

export function app(): FirebaseApp {
return new FirebaseApp(appOptions, appName);
export function app(altName?: string): FirebaseApp {
return new FirebaseApp(appOptions, altName || appName);
}

export function mockCredentialApp(): FirebaseApp {
Expand Down
Loading

0 comments on commit 1339d59

Please sign in to comment.