diff --git a/docs/storage/storage.md b/docs/storage/storage.md index 43b6c5a0b..37eb68a42 100644 --- a/docs/storage/storage.md +++ b/docs/storage/storage.md @@ -186,6 +186,16 @@ export class AppComponent { ### Downloading Files +A convenient pipe exists for simple in page references. + +```ts +@Component({ + selector: 'app-root', + template: `` +}) +export class AppComponent {} +``` + To download a file you'll need to create a reference and call the `getDownloadURL()` method on an `AngularFireStorageReference`. ```ts diff --git a/sample/src/app/storage/storage.component.ts b/sample/src/app/storage/storage.component.ts index b8e3547ee..2dc184ef1 100644 --- a/sample/src/app/storage/storage.component.ts +++ b/sample/src/app/storage/storage.component.ts @@ -13,7 +13,8 @@ const TRANSPARENT_PNG template: `

Storage! - + +
{{ 'google-g.png' | getDownloadURL | json }}

`, styles: [] diff --git a/src/storage/pipes/storageUrl.pipe.ts b/src/storage/pipes/storageUrl.pipe.ts new file mode 100644 index 000000000..1aaf2caeb --- /dev/null +++ b/src/storage/pipes/storageUrl.pipe.ts @@ -0,0 +1,39 @@ +import { AsyncPipe } from '@angular/common'; +import { ChangeDetectorRef, NgModule, OnDestroy, Pipe, PipeTransform } from '@angular/core'; +import { Observable } from 'rxjs'; +import { AngularFireStorage } from '../storage'; + +/** to be used with in combination with | async */ +@Pipe({ + name: 'getDownloadURL', + pure: false, +}) +export class GetDownloadURLPipe implements PipeTransform, OnDestroy { + + private asyncPipe: AsyncPipe; + private path: string; + private downloadUrl$: Observable; + + constructor(private storage: AngularFireStorage, cdr: ChangeDetectorRef) { + this.asyncPipe = new AsyncPipe(cdr); + } + + transform(path: string) { + if (path !== this.path) { + this.path = path; + this.downloadUrl$ = this.storage.ref(path).getDownloadURL(); + } + return this.asyncPipe.transform(this.downloadUrl$); + } + + ngOnDestroy() { + this.asyncPipe.ngOnDestroy(); + } + +} + +@NgModule({ + declarations: [ GetDownloadURLPipe ], + exports: [ GetDownloadURLPipe ], +}) +export class GetDownloadURLPipeModule {} diff --git a/src/storage/public_api.ts b/src/storage/public_api.ts index a2848a409..460348fa1 100644 --- a/src/storage/public_api.ts +++ b/src/storage/public_api.ts @@ -3,3 +3,4 @@ export * from './storage'; export * from './task'; export * from './observable/fromTask'; export * from './storage.module'; +export * from './pipes/storageUrl.pipe'; diff --git a/src/storage/storage.module.ts b/src/storage/storage.module.ts index 4169db29f..c6181645f 100644 --- a/src/storage/storage.module.ts +++ b/src/storage/storage.module.ts @@ -1,7 +1,9 @@ import { NgModule } from '@angular/core'; +import { GetDownloadURLPipeModule } from './pipes/storageUrl.pipe'; import { AngularFireStorage } from './storage'; @NgModule({ + exports: [ GetDownloadURLPipeModule ], providers: [ AngularFireStorage ] }) export class AngularFireStorageModule { } diff --git a/src/storage/storage.spec.ts b/src/storage/storage.spec.ts index 083d4b645..2f11fde08 100644 --- a/src/storage/storage.spec.ts +++ b/src/storage/storage.spec.ts @@ -1,26 +1,45 @@ -import { forkJoin } from 'rxjs'; +import { forkJoin, from } from 'rxjs'; import { mergeMap, tap } from 'rxjs/operators'; import { TestBed } from '@angular/core/testing'; import { AngularFireModule, FIREBASE_APP_NAME, FIREBASE_OPTIONS, FirebaseApp } from '@angular/fire'; import { AngularFireStorage, AngularFireStorageModule, AngularFireUploadTask, BUCKET } from './public_api'; import { COMMON_CONFIG } from '../test-config'; -import 'firebase/storage'; import { rando } from '../firestore/utils.spec'; +import { GetDownloadURLPipe } from './pipes/storageUrl.pipe'; +import { ChangeDetectorRef } from '@angular/core'; +import 'firebase/storage'; + +if (typeof XMLHttpRequest === 'undefined') { + globalThis.XMLHttpRequest = require('xhr2'); +} + +const blobOrBuffer = (data: string, options: {}) => { + if (typeof Blob === 'undefined') { + return Buffer.from(data, 'utf8'); + } else { + return new Blob([JSON.stringify(data)], options); + } +}; describe('AngularFireStorage', () => { let app: FirebaseApp; let afStorage: AngularFireStorage; + let cdr: ChangeDetectorRef; beforeEach(() => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG, rando()), - AngularFireStorageModule + AngularFireStorageModule, + ], + providers: [ + ChangeDetectorRef ] }); app = TestBed.inject(FirebaseApp); afStorage = TestBed.inject(AngularFireStorage); + cdr = TestBed.inject(ChangeDetectorRef); }); afterEach(() => { @@ -39,101 +58,96 @@ describe('AngularFireStorage', () => { expect(afStorage.storage.app).toBeDefined(); }); - // TODO tests for node? - if (typeof Blob !== 'undefined') { - - describe('upload task', () => { - - it('should upload and delete a file', (done) => { - const data = { angular: 'fire' }; - const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); - const ref = afStorage.ref('af.json'); - const task = ref.put(blob); - task.snapshotChanges() - .subscribe( - snap => { - expect(snap).toBeDefined(); - }, - done.fail, - () => { - ref.delete().subscribe(done, done.fail); - }); - }); - - it('should upload a file and observe the download url', (done) => { - const data = { angular: 'fire' }; - const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); - const ref = afStorage.ref('af.json'); - ref.put(blob).then(() => { - const url$ = ref.getDownloadURL(); - url$.subscribe( - url => { - expect(url).toBeDefined(); - }, - done.fail, - () => { - ref.delete().subscribe(done, done.fail); - } - ); - }); - }); + describe('upload task', () => { + + it('should upload and delete a file', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task = ref.put(blob); + task.snapshotChanges() + .subscribe( + snap => { + expect(snap).toBeDefined(); + }, + done.fail, + () => { + ref.delete().subscribe(done, done.fail); + }); + }); - it('should resolve the task as a promise', (done) => { - const data = { angular: 'promise' }; - const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); - const ref = afStorage.ref('af.json'); - const task: AngularFireUploadTask = ref.put(blob); - task.then(snap => { - expect(snap).toBeDefined(); - done(); - }).catch(done.fail); + it('should upload a file and observe the download url', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + ref.put(blob).then(() => { + const url$ = ref.getDownloadURL(); + url$.subscribe( + url => { + expect(url).toBeDefined(); + }, + done.fail, + () => { + ref.delete().subscribe(done, done.fail); + } + ); }); - }); - describe('reference', () => { + it('should resolve the task as a promise', (done) => { + const data = { angular: 'promise' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task: AngularFireUploadTask = ref.put(blob); + task.then(snap => { + expect(snap).toBeDefined(); + done(); + }).catch(done.fail); + }); - it('it should upload, download, and delete', (done) => { - const data = { angular: 'fire' }; - const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); - const ref = afStorage.ref('af.json'); - const task = ref.put(blob); - // Wait for the upload - forkJoin([task.snapshotChanges()]) - .pipe( - // get the url download - mergeMap(() => ref.getDownloadURL()), - // assert the URL - tap(url => expect(url).toBeDefined()), - // Delete the file - mergeMap(() => ref.delete()) - ) - // finish the test - .subscribe(done, done.fail); - }); + }); - it('should upload, get metadata, and delete', (done) => { - const data = { angular: 'fire' }; - const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); - const ref = afStorage.ref('af.json'); - const task = ref.put(blob, { customMetadata: { blah: 'blah' } }); - // Wait for the upload - forkJoin([task.snapshotChanges()]) - .pipe( - // get the metadata download - mergeMap(() => ref.getMetadata()), - // assert the URL - tap(meta => expect(meta.customMetadata).toEqual({ blah: 'blah' })), - // Delete the file - mergeMap(() => ref.delete()) - ) - // finish the test - .subscribe(done, done.fail); - }); + describe('reference', () => { + + it('it should upload, download, and delete', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task = ref.put(blob); + // Wait for the upload + forkJoin([task.snapshotChanges()]) + .pipe( + // get the url download + mergeMap(() => ref.getDownloadURL()), + // assert the URL + tap(url => expect(url).toBeDefined()), + // Delete the file + mergeMap(() => ref.delete()) + ) + // finish the test + .subscribe(done, done.fail); + }); + it('should upload, get metadata, and delete', (done) => { + const data = { angular: 'fire' }; + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); + const ref = afStorage.ref('af.json'); + const task = ref.put(blob, { customMetadata: { blah: 'blah' } }); + // Wait for the upload + forkJoin([task.snapshotChanges()]) + .pipe( + // get the metadata download + mergeMap(() => ref.getMetadata()), + // assert the URL + tap(meta => expect(meta.customMetadata).toEqual({ blah: 'blah' })), + // Delete the file + mergeMap(() => ref.delete()) + ) + // finish the test + .subscribe(done, done.fail); }); - } + }); }); @@ -193,7 +207,7 @@ describe('AngularFireStorage w/options', () => { it('it should upload, download, and delete', (done) => { const data = { angular: 'fire' }; - const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); + const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' }); const ref = afStorage.ref('af.json'); const task = ref.put(blob); // Wait for the upload